diff --git a/services/task/task.py b/services/task/task.py index 0c201bb4a2..67b153a436 100644 --- a/services/task/task.py +++ b/services/task/task.py @@ -324,6 +324,12 @@ def delete_timeseries(self, repository_id: int): kwargs=dict(repository_id=repository_id), ).apply_async() + def transplant_report(self, repo_id: int, from_sha: str, to_sha: str) -> None: + self._create_signature( + "app.tasks.reports.transplant_report", + kwargs={"repo_id": repo_id, "from_sha": from_sha, "to_sha": to_sha}, + ).apply_async() + def update_commit(self, commitid, repoid): self._create_signature( "app.tasks.commit_update.CommitUpdate", diff --git a/upload/tests/views/test_transplant_report.py b/upload/tests/views/test_transplant_report.py new file mode 100644 index 0000000000..2fc347961e --- /dev/null +++ b/upload/tests/views/test_transplant_report.py @@ -0,0 +1,34 @@ +from django.urls import reverse +from rest_framework.test import APIClient +from shared.django_apps.core.tests.factories import RepositoryFactory + +from upload.views.uploads import CanDoCoverageUploadsPermission + + +def test_uploads_get_not_allowed(db, mocker): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + task_mock = mocker.patch("services.task.TaskService.transplant_report") + + repository = RepositoryFactory( + name="the-repo", author__username="codecov", author__service="github" + ) + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + + url = reverse( + "new_upload.transplant_report", + args=["github", "codecov::::the-repo"], + ) + assert url == "/upload/github/codecov::::the-repo/commits/transplant" + + res = client.post( + url, data={"from_sha": "sha to copy from", "to_sha": "sha to copy to"} + ) + assert res.status_code == 200 + + task_mock.assert_called_once_with( + repo_id=repository.repoid, from_sha="sha to copy from", to_sha="sha to copy to" + ) diff --git a/upload/urls.py b/upload/urls.py index 5194d09c3c..adc6a17456 100644 --- a/upload/urls.py +++ b/upload/urls.py @@ -6,6 +6,7 @@ from upload.views.legacy import UploadDownloadHandler, UploadHandler from upload.views.reports import ReportResultsView, ReportViews from upload.views.test_results import TestResultsView +from upload.views.transplant_report import TransplantReportView from upload.views.upload_completion import UploadCompletionView from upload.views.upload_coverage import UploadCoverageView from upload.views.uploads import UploadViews @@ -58,6 +59,11 @@ CommitViews.as_view(), name="new_upload.commits", ), + path( + "//commits/transplant", + TransplantReportView.as_view(), + name="new_upload.transplant_report", + ), path( "//upload-coverage", UploadCoverageView.as_view(), diff --git a/upload/views/transplant_report.py b/upload/views/transplant_report.py new file mode 100644 index 0000000000..acc0cfe7a9 --- /dev/null +++ b/upload/views/transplant_report.py @@ -0,0 +1,69 @@ +import logging +from typing import Any, Callable + +from django.http import HttpRequest +from rest_framework import serializers, status +from rest_framework.generics import CreateAPIView +from rest_framework.response import Response +from shared.metrics import inc_counter + +from codecov_auth.authentication.repo_auth import ( + GitHubOIDCTokenAuthentication, + GlobalTokenAuthentication, + OrgLevelTokenAuthentication, + RepositoryLegacyTokenAuthentication, + UploadTokenRequiredAuthenticationCheck, + repo_auth_custom_exception_handler, +) +from services.task import TaskService +from upload.helpers import generate_upload_prometheus_metrics_labels +from upload.metrics import API_UPLOAD_COUNTER +from upload.views.base import GetterMixin +from upload.views.uploads import CanDoCoverageUploadsPermission + +log = logging.getLogger(__name__) + + +class TransplantReportSerializer(serializers.Serializer): + from_sha = serializers.CharField(required=True) + to_sha = serializers.CharField(required=True) + + +class TransplantReportView(CreateAPIView, GetterMixin): + permission_classes = [CanDoCoverageUploadsPermission] + authentication_classes = [ + UploadTokenRequiredAuthenticationCheck, + GlobalTokenAuthentication, + OrgLevelTokenAuthentication, + GitHubOIDCTokenAuthentication, + RepositoryLegacyTokenAuthentication, + ] + + def get_exception_handler(self) -> Callable[[Exception, dict[str, Any]], Response]: + return repo_auth_custom_exception_handler + + def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Response: + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="transplant_report", + request=self.request, + is_shelter_request=self.is_shelter_request(), + ), + ) + serializer = TransplantReportSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + data = serializer.validated_data + TaskService().transplant_report( + repo_id=self.get_repo().repoid, + from_sha=data["from_sha"], + to_sha=data["to_sha"], + ) + + return Response( + data={"result": "All good, transplant scheduled"}, + status=status.HTTP_200_OK, + )