diff --git a/readthedocs/api/v2/views/model_views.py b/readthedocs/api/v2/views/model_views.py index 529b498e37f..c274b801df4 100644 --- a/readthedocs/api/v2/views/model_views.py +++ b/readthedocs/api/v2/views/model_views.py @@ -336,6 +336,14 @@ def perform_create(self, serializer): build_api_key = self.request.build_api_key if not build_api_key.project.builds.filter(pk=build_pk).exists(): raise PermissionDenied() + + if BuildCommandResult.objects.filter( + build=serializer.validated_data["build"], + start_time=serializer.validated_data["start_time"], + ).exists(): + log.warning("Build command is duplicated. Skipping...") + return + return super().perform_create(serializer) def get_queryset_for_api_key(self, api_key): diff --git a/readthedocs/rtd_tests/tests/test_api.py b/readthedocs/rtd_tests/tests/test_api.py index f0da2e55767..f14e2c9ad00 100644 --- a/readthedocs/rtd_tests/tests/test_api.py +++ b/readthedocs/rtd_tests/tests/test_api.py @@ -909,6 +909,48 @@ def test_build_read_and_write_endpoints_for_build_api_token(self): resp = client.patch(f"/api/v2/build/{build.pk}/") self.assertEqual(resp.status_code, 404) + def test_build_commands_duplicated_command(self): + """Sending the same request twice should only create one BuildCommandResult.""" + project = get( + Project, + language="en", + ) + version = project.versions.first() + build = Build.objects.create(project=project, version=version) + + self.assertEqual(BuildCommandResult.objects.count(), 0) + + client = APIClient() + _, build_api_key = BuildAPIKey.objects.create_key(project) + client.credentials(HTTP_AUTHORIZATION=f"Token {build_api_key}") + + now = timezone.now() + start_time = now - datetime.timedelta(seconds=5) + end_time = now + + data = { + "build": build.pk, + "command": "git status", + "description": "Git status", + "exit_code": 0, + "start_time": start_time, + "end_time": end_time, + } + + response = client.post( + "/api/v2/command/", + data, + format="json", + ) + self.assertEqual(response.status_code, 201) + response = client.post( + "/api/v2/command/", + data, + format="json", + ) + self.assertEqual(response.status_code, 201) + self.assertEqual(BuildCommandResult.objects.count(), 1) + def test_build_commands_read_only_endpoints_for_normal_user(self): user_normal = get(User, is_staff=False) user_admin = get(User, is_staff=True)