Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions apps/jobs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,29 @@ def challenge_submission(request, challenge_id, challenge_phase_id):

if serializer.is_valid():
serializer.save()
response_data = serializer.data
submission = serializer.instance
message["submission_pk"] = submission.id
# publish message in the queue
publish_submission_message(message)

try:
publish_submission_message(message)
except Exception:
logger.exception(
"SQS publish failed for submission %s in challenge %s, "
"cancelling submission",
submission.pk,
challenge_id,
)
submission.status = Submission.CANCELLED
submission.save()
response_data = {
"error": "Failed to process your submission. Please try again."
}
return Response(
response_data,
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

response_data = serializer.data
return Response(response_data, status=status.HTTP_201_CREATED)
return Response(
serializer.errors, status=status.HTTP_406_NOT_ACCEPTABLE
Expand Down
137 changes: 137 additions & 0 deletions tests/unit/jobs/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import timedelta

import boto3
import botocore
import mock
import requests
from allauth.account.models import EmailAddress
Expand Down Expand Up @@ -532,6 +533,142 @@ def test_challenge_submission_for_docker_based_challenges(self):

self.assertEqual(response.status_code, status.HTTP_201_CREATED)

@mock.patch(
"jobs.views.publish_submission_message",
side_effect=Exception("SQS connection error"),
)
def test_challenge_submission_cleans_up_on_publish_failure(
self, mock_publish
):
self.url = reverse_lazy(
"jobs:challenge_submission",
kwargs={
"challenge_id": self.challenge.pk,
"challenge_phase_id": self.challenge_phase.pk,
},
)

self.challenge.participant_teams.add(self.participant_team)
self.challenge.save()

submission_count_before = Submission.objects.count()

response = self.client.post(
self.url,
{"status": "submitting", "input_file": self.input_file},
format="multipart",
)
self.assertEqual(
response.status_code,
status.HTTP_500_INTERNAL_SERVER_ERROR,
)
self.assertIn("error", response.data)
# Orphaned submission must be deleted
self.assertEqual(Submission.objects.count(), submission_count_before)

@mock.patch(
"jobs.views.publish_submission_message",
side_effect=botocore.exceptions.EndpointConnectionError(
endpoint_url="https://sqs.us-east-1.amazonaws.com"
),
)
def test_challenge_submission_handles_sqs_endpoint_failure(
self, mock_publish
):
self.url = reverse_lazy(
"jobs:challenge_submission",
kwargs={
"challenge_id": self.challenge.pk,
"challenge_phase_id": self.challenge_phase.pk,
},
)

self.challenge.participant_teams.add(self.participant_team)
self.challenge.save()

submission_count_before = Submission.objects.count()

response = self.client.post(
self.url,
{"status": "submitting", "input_file": self.input_file},
format="multipart",
)
self.assertEqual(
response.status_code,
status.HTTP_500_INTERNAL_SERVER_ERROR,
)
self.assertEqual(Submission.objects.count(), submission_count_before)

@mock.patch(
"jobs.views.publish_submission_message",
side_effect=Exception("SQS send failed"),
)
def test_challenge_submission_preserves_quota_on_publish_failure(
self, mock_publish
):
self.url = reverse_lazy(
"jobs:challenge_submission",
kwargs={
"challenge_id": self.challenge.pk,
"challenge_phase_id": self.challenge_phase.pk,
},
)

self.challenge.participant_teams.add(self.participant_team)
self.challenge.save()

# First attempt fails due to SQS
response = self.client.post(
self.url,
{"status": "submitting", "input_file": self.input_file},
format="multipart",
)
self.assertEqual(
response.status_code,
status.HTTP_500_INTERNAL_SERVER_ERROR,
)

# Participant's quota should not be consumed — retry must still be allowed
mock_publish.side_effect = None
mock_publish.return_value = None
retry_input = SimpleUploadedFile(
"retry_input.txt", b"file_content", content_type="text/plain"
)
response = self.client.post(
self.url,
{"status": "submitting", "input_file": retry_input},
format="multipart",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

@mock.patch("jobs.views.publish_submission_message")
def test_challenge_submission_returns_201_when_publish_succeeds(
self, mock_publish
):
self.url = reverse_lazy(
"jobs:challenge_submission",
kwargs={
"challenge_id": self.challenge.pk,
"challenge_phase_id": self.challenge_phase.pk,
},
)

self.challenge.participant_teams.add(self.participant_team)
self.challenge.save()

submission_count_before = Submission.objects.count()

response = self.client.post(
self.url,
{"status": "submitting", "input_file": self.input_file},
format="multipart",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(
Submission.objects.count(), submission_count_before + 1
)
mock_publish.assert_called_once()

def test_challenge_submission_when_file_url_is_none(self):
self.url = reverse_lazy(
"jobs:challenge_submission",
Expand Down