Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit 8dc986f

Browse files
Merge branch 'master' into dorian/fix-staging-deploy
2 parents 3a68206 + 3daf2cc commit 8dc986f

File tree

9 files changed

+303
-10
lines changed

9 files changed

+303
-10
lines changed

codecov/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"<str:service>/<str:owner_username>/<str:repo_name>/",
2929
include("graphs.urls"),
3030
),
31-
path("upload/<str:version>", include("upload.urls")),
31+
path("upload/", include("upload.urls")),
3232
]
3333

3434
if not settings.IS_ENTERPRISE:
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from asgiref.sync import sync_to_async
2+
from codecov.commands.base import BaseInteractor
3+
from codecov_auth.models import Owner
4+
5+
class FetchOwnerInteractor(BaseInteractor):
6+
@sync_to_async
7+
def execute(self, username):
8+
return Owner.objects.filter(username=username, service=self.service).first()

codecov_auth/commands/owner/owner.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .interactors.set_yaml_on_owner import SetYamlOnOwnerInteractor
66
from .interactors.delete_session import DeleteSessionInteractor
77
from .interactors.update_profile import UpdateProfileInteractor
8+
from .interactors.fetch_owner import FetchOwnerInteractor
89

910

1011
class OwnerCommands(BaseCommand):
@@ -19,3 +20,6 @@ def set_yaml_on_owner(self, username, yaml):
1920

2021
def update_profile(self, **kwargs):
2122
return self.get_interactor(UpdateProfileInteractor).execute(**kwargs)
23+
24+
def fetch_owner(self, username):
25+
return self.get_interactor(FetchOwnerInteractor).execute(username)

reports/models.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import uuid
22

3+
from django.urls import reverse
4+
35
from django.db import models
46
from django.contrib.postgres.fields import ArrayField
57

@@ -98,12 +100,17 @@ class Meta:
98100
@property
99101
def download_url(self):
100102
repository = self.report.commit.repository
101-
owner = repository.author
102-
short_service = get_short_service_name(owner.service)
103-
path_download = (
104-
f"/api/{short_service}/{owner.username}/{repository.name}/download/build"
103+
return (
104+
reverse(
105+
"upload-download",
106+
kwargs={
107+
"service": get_short_service_name(repository.author.service),
108+
"owner_username": repository.author.username,
109+
"repo_name": repository.name,
110+
},
111+
)
112+
+ f"?path={self.storage_path}"
105113
)
106-
return f"{path_download}?path={self.storage_path}"
107114

108115
@property
109116
def ci_url(self):

reports/tests/test_models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def test_get_download_url(self):
1414
repository = session.report.commit.repository
1515
assert (
1616
session.download_url
17-
== f"/api/gh/{repository.author.username}/{repository.name}/download/build?path={storage_path}"
17+
== f"/upload/gh/{repository.author.username}/{repository.name}/download?path={storage_path}"
1818
)
1919

2020
def test_ci_url_when_no_provider(self):

upload/tests/test_upload.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,19 @@ def _post(
944944
url = reverse("upload-handler", kwargs=kwargs) + query_string
945945
return self.client.post(url, data=data, content_type=content_type, **headers)
946946

947+
def _post_slash(
948+
self,
949+
kwargs=None,
950+
data=None,
951+
query=None,
952+
content_type="application/json",
953+
headers=None,
954+
):
955+
headers = headers or {}
956+
query_string = f"?{urlencode(query)}" if query else ""
957+
url = "/upload/v2/" + query_string
958+
return self.client.post(url, data=data, content_type=content_type, **headers)
959+
947960
def setUp(self):
948961
self.org = G(Owner, username="codecovtest", service="github")
949962
self.repo = G(
@@ -1067,6 +1080,85 @@ async def get_commit(self, commit, token):
10671080
== "https://codecov.io/github/codecovtest/upload-test-repo/commit/b521e55aef79b101f48e2544837ca99a7fa3bf6b"
10681081
)
10691082

1083+
@patch("upload.views.get_redis_connection")
1084+
@patch("upload.views.uuid4")
1085+
@patch("upload.views.dispatch_upload_task")
1086+
@patch("services.repo_providers.RepoProviderService.get_adapter")
1087+
def test_successful_upload_v2_slash(
1088+
self,
1089+
mock_repo_provider_service,
1090+
mock_dispatch_upload,
1091+
mock_uuid4,
1092+
mock_get_redis,
1093+
):
1094+
class MockRepoProviderAdapter:
1095+
async def get_commit(self, commit, token):
1096+
return {"message": "This is not a merge commit"}
1097+
1098+
mock_get_redis.return_value = MockRedis()
1099+
mock_repo_provider_service.return_value = MockRepoProviderAdapter()
1100+
mock_uuid4.return_value = (
1101+
"dec1f00b-1883-40d0-afd6-6dcb876510be" # this will be the reportid
1102+
)
1103+
1104+
query_params = {
1105+
"commit": "b521e55aef79b101f48e2544837ca99a7fa3bf6b",
1106+
"token": "test27s4f3uz3ha9pi0foipg5bqojtrmbt67",
1107+
"pr": "456",
1108+
"branch": "",
1109+
"flags": "",
1110+
"build_url": "",
1111+
}
1112+
1113+
response = self._post_slash(
1114+
kwargs={"version": "v2"}, query=query_params, data="coverage report"
1115+
)
1116+
1117+
assert response.status_code == 200
1118+
1119+
headers = response._headers
1120+
1121+
assert headers["access-control-allow-origin"] == (
1122+
"Access-Control-Allow-Origin",
1123+
"*",
1124+
)
1125+
assert headers["access-control-allow-headers"] == (
1126+
"Access-Control-Allow-Headers",
1127+
"Origin, Content-Type, Accept, X-User-Agent",
1128+
)
1129+
assert headers["content-type"] != (
1130+
"Content-Type",
1131+
"text/plain",
1132+
)
1133+
1134+
assert mock_dispatch_upload.call_args[0][0] == {
1135+
"commit": "b521e55aef79b101f48e2544837ca99a7fa3bf6b",
1136+
"token": "test27s4f3uz3ha9pi0foipg5bqojtrmbt67",
1137+
"pr": "456",
1138+
"version": "v2",
1139+
"service": None,
1140+
"owner": None,
1141+
"repo": None,
1142+
"using_global_token": False,
1143+
"build_url": None,
1144+
"branch": None,
1145+
"reportid": "dec1f00b-1883-40d0-afd6-6dcb876510be",
1146+
"redis_key": "upload/b521e55/dec1f00b-1883-40d0-afd6-6dcb876510be/plain",
1147+
"url": None,
1148+
"branch": None,
1149+
"job": None,
1150+
}
1151+
1152+
result = loads(response.content)
1153+
assert result["message"] == "Coverage reports upload successfully"
1154+
assert result["uploaded"] == True
1155+
assert result["queued"] == True
1156+
assert result["id"] == "dec1f00b-1883-40d0-afd6-6dcb876510be"
1157+
assert (
1158+
result["url"]
1159+
== "https://codecov.io/github/codecovtest/upload-test-repo/commit/b521e55aef79b101f48e2544837ca99a7fa3bf6b"
1160+
)
1161+
10701162
@patch("services.storage.MINIO_CLIENT.presigned_put_object")
10711163
@patch("services.archive.ArchiveService.get_archive_hash")
10721164
@patch("upload.views.get_redis_connection")

upload/tests/test_upload_download.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import minio
2+
from ddf import G
3+
from unittest.mock import patch
4+
from core.models import Repository
5+
from codecov_auth.models import Owner
6+
from rest_framework.reverse import reverse
7+
from rest_framework.test import APITestCase
8+
9+
10+
class UploadDownloadHelperTest(APITestCase):
11+
def _get(self, kwargs={}, data={}):
12+
path = f"/upload/{kwargs.get('service')}/{kwargs.get('owner_username')}/{kwargs.get('repo_name')}/download"
13+
return self.client.get(path, data=data)
14+
15+
def setUp(self):
16+
self.org = G(Owner, username="codecovtest", service="github")
17+
self.repo = G(
18+
Repository,
19+
author=self.org,
20+
name="upload-test-repo",
21+
upload_token="test27s4f3uz3ha9pi0foipg5bqojtrmbt67",
22+
)
23+
self.repo = G(
24+
Repository, author=self.org, name="private-upload-test-repo", private=True
25+
)
26+
27+
def test_no_path_param(self):
28+
response = self._get()
29+
assert response.status_code == 404
30+
31+
def test_invalid_path_param(self):
32+
response = self._get(data={"path": "v2"})
33+
assert response.status_code == 404
34+
35+
def test_invalid_owner(self):
36+
response = self._get(
37+
kwargs={
38+
"service": "gh",
39+
"owner_username": "invalid",
40+
"repo_name": "invalid",
41+
},
42+
data={"path": "v4/raw"},
43+
)
44+
assert response.status_code == 404
45+
46+
def test_invalid_repo(self):
47+
response = self._get(
48+
kwargs={
49+
"service": "gh",
50+
"owner_username": "codecovtest",
51+
"repo_name": "invalid",
52+
},
53+
data={"path": "v4/raw"},
54+
)
55+
assert response.status_code == 404
56+
57+
@patch("services.archive.ArchiveService.get_archive_hash")
58+
@patch("services.archive.ArchiveService.read_file")
59+
def test_invalid_archive_path(self, read_file, get_archive_hash):
60+
read_file.side_effect = [minio.error.NoSuchKey]
61+
get_archive_hash.return_value = "path"
62+
response = self._get(
63+
kwargs={
64+
"service": "gh",
65+
"owner_username": "codecovtest",
66+
"repo_name": "upload-test-repo",
67+
},
68+
data={"path": "v4/raw/path"},
69+
)
70+
assert response.status_code == 404
71+
72+
@patch("services.archive.ArchiveService.get_archive_hash")
73+
@patch("services.archive.ArchiveService.read_file")
74+
def test_valid_repo_archive_path(self, mock_read_file, get_archive_hash):
75+
mock_read_file.return_value = "Report!"
76+
get_archive_hash.return_value = "hasssshhh"
77+
response = self._get(
78+
kwargs={
79+
"service": "gh",
80+
"owner_username": "codecovtest",
81+
"repo_name": "upload-test-repo",
82+
},
83+
data={"path": "v4/raw/hasssshhh"},
84+
)
85+
assert response.status_code == 200
86+
headers = response._headers
87+
assert headers["content-type"] == ("Content-Type", "text/plain")
88+
89+
@patch("services.archive.ArchiveService.read_file")
90+
def test_invalid_repo_archive_path(self, mock_read_file):
91+
mock_read_file.return_value = "Report!"
92+
response = self._get(
93+
kwargs={
94+
"service": "gh",
95+
"owner_username": "codecovtest",
96+
"repo_name": "upload-test-repo",
97+
},
98+
data={"path": "v4/raw"},
99+
)
100+
assert response.status_code == 404
101+
102+
def test_private_valid_archive_path(self):
103+
response = self._get(
104+
kwargs={
105+
"service": "gh",
106+
"owner_username": "codecovtest",
107+
"repo_name": "private-upload-test-repo",
108+
},
109+
data={"path": "v4/raw"},
110+
)
111+
assert response.status_code == 404

upload/urls.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
from django.urls import re_path
2-
from .views import UploadHandler
1+
from django.urls import path, re_path
2+
from .views import UploadHandler, UploadDownloadHandler
33

44

55
urlpatterns = [
66
# use regex to make trailing slash optional
7-
re_path("^/?", UploadHandler.as_view(), name="upload-handler"),
7+
path(
8+
"<str:service>/<str:owner_username>/<str:repo_name>/download",
9+
UploadDownloadHandler.as_view(),
10+
name="upload-download",
11+
),
12+
re_path("(?P<version>\w+)/?", UploadHandler.as_view(), name="upload-handler"),
813
]

upload/views.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import logging
2+
import asyncio
3+
4+
import minio
5+
from django.http import Http404
6+
from utils.services import get_long_service_name
27
from datetime import datetime
38
from rest_framework import status, renderers
49
from rest_framework.views import APIView
10+
from django.views import View
511
from rest_framework.permissions import AllowAny
612
from rest_framework.exceptions import ValidationError
713
from django.http import HttpResponse, HttpResponseServerError
@@ -10,6 +16,11 @@
1016
from json import dumps
1117
from uuid import uuid4
1218
from django.utils import timezone
19+
from django.utils.decorators import classonlymethod
20+
from asgiref.sync import sync_to_async
21+
22+
from core.commands.repository import RepositoryCommands
23+
from codecov_auth.commands.owner import OwnerCommands
1324

1425
from .helpers import (
1526
parse_params,
@@ -297,3 +308,58 @@ def post(self, request, *args, **kwargs):
297308

298309
response.status_code = status.HTTP_200_OK
299310
return response
311+
312+
313+
class UploadDownloadHandler(View):
314+
@classonlymethod
315+
def as_view(_, **initkwargs):
316+
view = super().as_view(**initkwargs)
317+
view._is_coroutine = asyncio.coroutines._is_coroutine
318+
return view
319+
320+
async def get_repo(self):
321+
owner = await OwnerCommands(self.request.user, self.service).fetch_owner(
322+
self.owner_username
323+
)
324+
if owner is None:
325+
raise Http404("Requested report could not be found")
326+
repo = await RepositoryCommands(
327+
self.request.user, self.service
328+
).fetch_repository(owner, self.repo_name)
329+
if repo is None:
330+
raise Http404("Requested report could not be found")
331+
return repo
332+
333+
def validate_path(self):
334+
if not self.path or "v4/raw" not in self.path:
335+
raise Http404("Requested report could not be found")
336+
337+
def read_params(self):
338+
self.path = self.request.GET.get("path")
339+
self.service = get_long_service_name(self.kwargs.get("service"))
340+
self.repo_name = self.kwargs.get("repo_name")
341+
self.owner_username = self.kwargs.get("owner_username")
342+
343+
@sync_to_async
344+
def get_from_storage(self, repo):
345+
archive_service = ArchiveService(repo)
346+
347+
# Verify that the repo hash in the path matches the repo in the URL by generating the repo hash
348+
if archive_service.storage_hash not in self.path:
349+
raise Http404("Requested report could not be found")
350+
try:
351+
return archive_service.read_file(self.path)
352+
353+
except minio.error.NoSuchKey as e:
354+
raise Http404("Requested report could not be found")
355+
356+
async def get(self, request, *args, **kwargs):
357+
self.read_params()
358+
self.validate_path()
359+
360+
repo = await self.get_repo()
361+
raw_uploaded_report = await self.get_from_storage(repo)
362+
363+
response = HttpResponse(raw_uploaded_report)
364+
response["Content-Type"] = "text/plain"
365+
return response

0 commit comments

Comments
 (0)