diff --git a/graphs/tests/test_badge_handler.py b/graphs/tests/test_badge_handler.py index e79f7d1214..c8b47ccf28 100644 --- a/graphs/tests/test_badge_handler.py +++ b/graphs/tests/test_badge_handler.py @@ -143,7 +143,7 @@ def test_invalid_extension(self): response.data["detail"] == "File extension should be one of [ svg || txt ]" ) - def test_unknown_bagde_incorrect_service(self): + def test_unknown_badge_incorrect_service(self): response = self._get( kwargs={ "service": "gih", @@ -182,7 +182,7 @@ def test_unknown_bagde_incorrect_service(self): assert expected_badge == badge assert response.status_code == status.HTTP_200_OK - def test_unknown_bagde_incorrect_owner(self): + def test_unknown_badge_incorrect_owner(self): response = self._get( kwargs={ "service": "gh", @@ -221,7 +221,7 @@ def test_unknown_bagde_incorrect_owner(self): assert expected_badge == badge assert response.status_code == status.HTTP_200_OK - def test_unknown_bagde_incorrect_repo(self): + def test_unknown_badge_incorrect_repo(self): gh_owner = OwnerFactory(service="github") response = self._get( kwargs={ @@ -261,7 +261,7 @@ def test_unknown_bagde_incorrect_repo(self): assert expected_badge == badge assert response.status_code == status.HTTP_200_OK - def test_unknown_bagde_no_branch(self): + def test_unknown_badge_no_branch(self): gh_owner = OwnerFactory(service="github") RepositoryFactory(author=gh_owner, active=True, private=False, name="repo1") response = self._get( @@ -302,7 +302,7 @@ def test_unknown_bagde_no_branch(self): assert expected_badge == badge assert response.status_code == status.HTTP_200_OK - def test_unknown_bagde_no_commit(self): + def test_unknown_badge_no_commit(self): gh_owner = OwnerFactory(service="github") repo = RepositoryFactory( author=gh_owner, active=True, private=False, name="repo1" @@ -346,7 +346,7 @@ def test_unknown_bagde_no_commit(self): assert expected_badge == badge assert response.status_code == status.HTTP_200_OK - def test_unknown_bagde_no_totals(self): + def test_unknown_badge_no_totals(self): gh_owner = OwnerFactory(service="github") repo = RepositoryFactory( author=gh_owner, active=True, private=False, name="repo1" diff --git a/graphs/tests/test_bundle_badge_handler.py b/graphs/tests/test_bundle_badge_handler.py new file mode 100644 index 0000000000..0317ab05b2 --- /dev/null +++ b/graphs/tests/test_bundle_badge_handler.py @@ -0,0 +1,540 @@ +from unittest.mock import patch + +from rest_framework import status +from rest_framework.test import APITestCase +from shared.bundle_analysis import BundleAnalysisReport, BundleReport +from shared.django_apps.core.tests.factories import ( + BranchFactory, + CommitFactory, + OwnerFactory, + RepositoryFactory, +) + + +class MockBundleReport(BundleReport): + def __init__(self): + return + + def total_size(self): + return 1234567 + + +class MockBundleAnalysisReport(BundleAnalysisReport): + def bundle_report(self, bundle_name: str): + if bundle_name == "idk": + return None + return MockBundleReport() + + +class TestBundleBadgeHandler(APITestCase): + def _get(self, kwargs={}, data={}): + path = f"/{kwargs.get('service')}/{kwargs.get('owner_username')}/{kwargs.get('repo_name')}/graphs/bundle/{kwargs.get('bundle')}/badge.{kwargs.get('ext')}" + return self.client.get(path, data=data) + + def _get_branch(self, kwargs={}, data={}): + path = f"/{kwargs.get('service')}/{kwargs.get('owner_username')}/{kwargs.get('repo_name')}/branch/{kwargs.get('branch')}/graphs/{kwargs.get('bundle')}/badge.{kwargs.get('ext')}" + return self.client.get(path, data=data) + + def test_invalid_extension(self): + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "user", + "repo_name": "repo", + "ext": "png", + "bundle": "asdf", + } + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert ( + response.data["detail"] == "File extension should be one of [ svg || txt ]" + ) + + def test_unknown_badge_incorrect_service(self): + response = self._get( + kwargs={ + "service": "gih", + "owner_username": "user", + "repo_name": "repo", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_unknown_badge_incorrect_owner(self): + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "user1233", + "repo_name": "repo", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_unknown_badge_incorrect_repo(self): + gh_owner = OwnerFactory(service="github") + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_unknown_badge_private_repo_wrong_token(self): + gh_owner = OwnerFactory(service="github") + RepositoryFactory( + author=gh_owner, active=True, private=True, name="repo1", image_token="asdf" + ) + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_unknown_badge_no_branch(self): + gh_owner = OwnerFactory(service="github") + RepositoryFactory(author=gh_owner, active=True, private=False, name="repo1") + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_unknown_badge_no_commit(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + branch = BranchFactory(name="main", repository=repo) + repo.branch = branch + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("graphs.views.load_report") + def test_unknown_badge_no_report(self, mock_load_report): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + branch = BranchFactory(name="main", repository=repo) + repo.branch = branch + commit = CommitFactory( + repository=repo, commitid=repo.branch.head, branch="main" + ) + branch.head = commit.commitid + + mock_load_report.return_value = None + + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("graphs.views.load_report") + def test_unknown_badge_no_bundle(self, mock_load_report): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + branch = BranchFactory(name="main", repository=repo) + repo.branch = branch + commit = CommitFactory( + repository=repo, commitid=repo.branch.head, branch="main" + ) + branch.head = commit.commitid + + mock_load_report.return_value = MockBundleAnalysisReport() + + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "bundle": "idk", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("graphs.views.load_report") + def test_bundle_badge(self, mock_load_report): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + branch = BranchFactory(name="main", repository=repo) + repo.branch = branch + commit = CommitFactory( + repository=repo, commitid=repo.branch.head, branch="main" + ) + branch.head = commit.commitid + + mock_load_report.return_value = MockBundleAnalysisReport() + + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + 1.23MB + 1.23MB + + +""" + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("graphs.views.load_report") + def test_bundle_badge_text(self, mock_load_report): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + branch = BranchFactory(name="main", repository=repo) + repo.branch = branch + commit = CommitFactory( + repository=repo, commitid=repo.branch.head, branch="main" + ) + branch.head = commit.commitid + + mock_load_report.return_value = MockBundleAnalysisReport() + + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "txt", + "bundle": "asdf", + } + ) + expected_badge = "1.23MB" + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("graphs.views.load_report") + def test_bundle_badge_unsupported_precision_defaults_to_2(self, mock_load_report): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + branch = BranchFactory(name="main", repository=repo) + repo.branch = branch + commit = CommitFactory( + repository=repo, commitid=repo.branch.head, branch="main" + ) + branch.head = commit.commitid + + mock_load_report.return_value = MockBundleAnalysisReport() + + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "txt", + "bundle": "asdf", + }, + data={"precision": "asdf"}, + ) + expected_badge = "1.23MB" + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + + @patch("graphs.views.load_report") + def test_bundle_badge_private_repo_correct_token(self, mock_load_report): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=True, name="repo1", image_token="asdf" + ) + branch = BranchFactory(name="main", repository=repo) + repo.branch = branch + commit = CommitFactory( + repository=repo, commitid=repo.branch.head, branch="main" + ) + branch.head = commit.commitid + + mock_load_report.return_value = MockBundleAnalysisReport() + + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "txt", + "bundle": "asdf", + }, + data={"token": "asdf"}, + ) + expected_badge = "1.23MB" + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK diff --git a/graphs/urls.py b/graphs/urls.py index bc0e0d45c8..cf069e9a42 100644 --- a/graphs/urls.py +++ b/graphs/urls.py @@ -1,6 +1,6 @@ from django.urls import re_path -from .views import BadgeHandler, GraphHandler +from .views import BadgeHandler, BundleBadgeHandler, GraphHandler urlpatterns = [ re_path( @@ -13,6 +13,16 @@ BadgeHandler.as_view(), name="default-badge", ), + re_path( + "branch/(?P.+)/(graph|graphs)/bundle/(?P.+)/badge.(?P[^/]+)", + BundleBadgeHandler.as_view(), + name="branch-bundle-badge", + ), + re_path( + "(graph|graphs)/bundle/(?P.+)/badge.(?P[^/]+)", + BundleBadgeHandler.as_view(), + name="default-bundle-badge", + ), re_path( "pull/(?P[^/]+)/(graph|graphs)/(?Ptree|icicle|sunburst|commits).(?P[^/]+)", GraphHandler.as_view(), diff --git a/graphs/views.py b/graphs/views.py index 9eb9c52714..68f0779aeb 100644 --- a/graphs/views.py +++ b/graphs/views.py @@ -14,9 +14,15 @@ from api.shared.mixins import RepoPropertyMixin from core.models import Branch, Pull from graphs.settings import settings +from services.bundle_analysis import load_report from services.components import commit_components -from .helpers.badge import format_coverage_precision, get_badge +from .helpers.badge import ( + format_bundle_bytes, + format_coverage_precision, + get_badge, + get_bundle_badge, +) from .helpers.graphs import icicle, sunburst, tree from .mixins import GraphBadgeAPIMixin @@ -193,6 +199,83 @@ def component_coverage(self, component_identifier: str, commit: Commit): return filtered_report.totals.coverage +class BundleBadgeHandler(APIView, RepoPropertyMixin, GraphBadgeAPIMixin): + content_negotiation_class = IgnoreClientContentNegotiation + + permission_classes = [AllowAny] + + extensions = ["svg", "txt"] + precisions = ["0", "1", "2"] + filename = "bundle-badge" + + def get_object(self, request, *args, **kwargs): + # Validate precision query param + precision = self.request.query_params.get("precision", "2") + precision = int(precision) if precision in self.precisions else 2 + + bundle_size_bytes = self.get_bundle_size() + + if self.kwargs.get("ext") == "txt": + return ( + "unknown" + if bundle_size_bytes is None + else format_bundle_bytes(bundle_size_bytes, precision) + ) + + return get_bundle_badge(bundle_size_bytes, precision) + + def get_bundle_size(self) -> int | None: + try: + repo = self.repo + except Http404: + log.warning("Repo not found", extra=dict(repo=self.kwargs.get("repo_name"))) + return None + + if repo.private and repo.image_token != self.request.query_params.get("token"): + log.warning( + "Token provided does not match repo's image token", + extra=dict(repo=repo), + ) + return None + + branch_name = self.kwargs.get("branch") or repo.branch + branch = Branch.objects.filter( + name=branch_name, repository_id=repo.repoid + ).first() + + if branch is None: + log.warning( + "Branch not found", extra=dict(branch_name=branch_name, repo=repo) + ) + return None + + commit: Commit = repo.commits.filter(commitid=branch.head).first() + if commit is None: + log.warning("Commit not found", extra=dict(commit=branch.head)) + return None + + commit_bundles = load_report(commit) + + if commit_bundles is None: + log.warning( + "Bundle analysis report not found for commit", + extra=dict(commit=branch.head), + ) + return None + + bundle_name = str(self.kwargs.get("bundle")) + bundle = commit_bundles.bundle_report(bundle_name) + + if bundle is None: + log.warning( + "Bundle with provided name not found for commit", + extra=dict(commit=branch.head), + ) + return None + + return bundle.total_size() + + class GraphHandler(APIView, RepoPropertyMixin, GraphBadgeAPIMixin): permission_classes = [AllowAny]