Skip to content

Commit 35a6525

Browse files
authored
Add Scan realted actions in REST API packages endpoint
Signed-off-by: tdruez <[email protected]>
1 parent 4fa1288 commit 35a6525

File tree

8 files changed

+252
-71
lines changed

8 files changed

+252
-71
lines changed

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ Release notes
77
https://github.com/aboutcode-org/dejacode/pull/315
88
https://github.com/aboutcode-org/dejacode/pull/312
99

10+
- Add REST API "actions" in package endpoint to track the scan status and fetch results:
11+
* `/packages/{uuid}/scan_info/` Scan information including the current status.
12+
* `/packages/{uuid}/scan_results/` Scan results.
13+
* `/packages/{uuid}/scan_summary/` Scan summary.
14+
* `/packages/{uuid}/scan_data_download_zip/` Download all scan data: results and
15+
summary, as a zip file.
16+
https://github.com/aboutcode-org/dejacode/issues/272
17+
1018
- Add new `is_locked` "Locked inventory" field to the ProductStatus model.
1119
When a Product is locked through his status, its inventory cannot be modified.
1220
https://github.com/aboutcode-org/dejacode/issues/189

component_catalog/api.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010

1111
from django.db import transaction
1212
from django.forms.widgets import HiddenInput
13+
from django.http import FileResponse
1314

1415
import django_filters
1516
from packageurl.contrib import url2purl
1617
from packageurl.contrib.django.filters import PackageURLFilter
1718
from rest_framework import serializers
19+
from rest_framework import status
1820
from rest_framework.decorators import action
21+
from rest_framework.exceptions import APIException
1922
from rest_framework.fields import ListField
2023
from rest_framework.response import Response
2124

@@ -864,6 +867,16 @@ def collect_create_scan(download_url, user):
864867
return package
865868

866869

870+
class ScanCodeUnavailable(APIException):
871+
status_code = status.HTTP_400_BAD_REQUEST
872+
default_detail = "The ScanCode.io service is not available"
873+
874+
875+
class ScanDataUnavailable(APIException):
876+
status_code = status.HTTP_400_BAD_REQUEST
877+
default_detail = "Scan data is not available"
878+
879+
867880
class PackageViewSet(
868881
SendAboutFilesMixin,
869882
AboutCodeFilesActionMixin,
@@ -919,6 +932,73 @@ def about(self, request, uuid):
919932
package = self.get_object()
920933
return Response({"about_data": package.as_about_yaml()})
921934

935+
def _get_scancodeio_project_info(self, scancodeio, package):
936+
if not scancodeio.is_available():
937+
raise ScanCodeUnavailable()
938+
939+
project_info = scancodeio.get_project_info(download_url=package.download_url)
940+
if not project_info:
941+
raise ScanDataUnavailable()
942+
943+
return project_info
944+
945+
@action(detail=True, name="Scan informations")
946+
def scan_info(self, request, uuid):
947+
"""Return information about the scan from ScanCode.io."""
948+
package = self.get_object()
949+
dataspace = request.user.dataspace
950+
scancodeio = ScanCodeIO(dataspace)
951+
project_info = self._get_scancodeio_project_info(scancodeio, package)
952+
953+
return Response(project_info)
954+
955+
@action(detail=True, name="Scan results")
956+
def scan_results(self, request, uuid):
957+
"""Return the scan results from ScanCode.io."""
958+
package = self.get_object()
959+
dataspace = request.user.dataspace
960+
scancodeio = ScanCodeIO(dataspace)
961+
project_info = self._get_scancodeio_project_info(scancodeio, package)
962+
963+
project_uuid = project_info.get("uuid")
964+
scan_results_url = scancodeio.get_scan_action_url(project_uuid, "results")
965+
scan_results = scancodeio.fetch_scan_data(scan_results_url)
966+
967+
return Response(scan_results)
968+
969+
@action(detail=True, name="Scan summary")
970+
def scan_summary(self, request, uuid):
971+
"""Return the scan summary from ScanCode.io."""
972+
package = self.get_object()
973+
dataspace = request.user.dataspace
974+
scancodeio = ScanCodeIO(dataspace)
975+
project_info = self._get_scancodeio_project_info(scancodeio, package)
976+
977+
project_uuid = project_info.get("uuid")
978+
scan_summary_url = scancodeio.get_scan_action_url(project_uuid, "summary")
979+
scan_summary = scancodeio.fetch_scan_data(scan_summary_url)
980+
981+
return Response(scan_summary)
982+
983+
@action(detail=True, name="Scan data (download as .zip)")
984+
def scan_data_download_zip(self, request, uuid):
985+
"""Download scan data: results and summary, as a zip file."""
986+
package = self.get_object()
987+
dataspace = request.user.dataspace
988+
scancodeio = ScanCodeIO(dataspace)
989+
project_info = self._get_scancodeio_project_info(scancodeio, package)
990+
991+
project_uuid = project_info.get("uuid")
992+
filename = package.filename or package.package_url_filename
993+
994+
scan_data_as_zip = scancodeio.scan_data_as_zip(project_uuid, filename)
995+
return FileResponse(
996+
scan_data_as_zip,
997+
filename=f"{filename}_scan.zip",
998+
as_attachment=True,
999+
content_type="application/zip",
1000+
)
1001+
9221002
@action(detail=False, methods=["post"], name="Package Add")
9231003
def add(self, request):
9241004
"""

component_catalog/tests/test_api.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,6 +1475,101 @@ def test_api_package_viewset_aboutcode_files_action(self):
14751475
'attachment; filename="package1.zip_about.zip"', response["content-disposition"]
14761476
)
14771477

1478+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.get_project_info")
1479+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.is_available")
1480+
def test_api_package_viewset_scan_actions_errors(
1481+
self, mock_is_available, mock_get_project_info
1482+
):
1483+
scan_actions_urls = [
1484+
reverse("api_v2:package-scan-info", args=[self.package1.uuid]),
1485+
reverse("api_v2:package-scan-results", args=[self.package1.uuid]),
1486+
reverse("api_v2:package-scan-summary", args=[self.package1.uuid]),
1487+
reverse("api_v2:package-scan-data-download-zip", args=[self.package1.uuid]),
1488+
]
1489+
1490+
mock_get_project_info.return_value = None
1491+
for action_url in scan_actions_urls:
1492+
response = self.client.get(action_url)
1493+
self.assertEqual(403, response.status_code, msg=action_url)
1494+
response = self.client.post(action_url)
1495+
self.assertEqual(403, response.status_code, msg=action_url)
1496+
1497+
self.client.login(username=self.base_user.username, password="secret")
1498+
mock_is_available.return_value = False
1499+
for action_url in scan_actions_urls:
1500+
response = self.client.get(action_url)
1501+
self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code, msg=action_url)
1502+
expected = "The ScanCode.io service is not available"
1503+
self.assertEqual(expected, str(response.data["detail"]), msg=action_url)
1504+
1505+
mock_is_available.return_value = True
1506+
for action_url in scan_actions_urls:
1507+
response = self.client.get(action_url)
1508+
self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code, msg=action_url)
1509+
expected = "Scan data is not available"
1510+
self.assertEqual(expected, str(response.data["detail"]), msg=action_url)
1511+
1512+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.get_project_info")
1513+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.is_available")
1514+
def test_api_package_viewset_scan_info_action(self, mock_is_available, mock_get_project_info):
1515+
self.client.login(username=self.base_user.username, password="secret")
1516+
action_url = reverse("api_v2:package-scan-info", args=[self.package1.uuid])
1517+
mock_is_available.return_value = True
1518+
project_info = {"uuid": "abcdef"}
1519+
mock_get_project_info.return_value = project_info
1520+
1521+
response = self.client.get(action_url)
1522+
self.assertEqual(200, response.status_code)
1523+
self.assertEqual(project_info, response.data)
1524+
1525+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.get_project_info")
1526+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_scan_data")
1527+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.is_available")
1528+
def test_api_package_viewset_scan_results_action(
1529+
self, mock_is_available, mock_fetch_scan_data, mock_get_project_info
1530+
):
1531+
self.client.login(username=self.base_user.username, password="secret")
1532+
action_url = reverse("api_v2:package-scan-results", args=[self.package1.uuid])
1533+
mock_is_available.return_value = True
1534+
mock_get_project_info.return_value = {"uuid": "abcdef"}
1535+
mock_fetch_scan_data.return_value = {"results": ""}
1536+
response = self.client.get(action_url)
1537+
self.assertEqual(200, response.status_code)
1538+
self.assertEqual({"results": ""}, response.data)
1539+
1540+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.get_project_info")
1541+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_scan_data")
1542+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.is_available")
1543+
def test_api_package_viewset_scan_summary_action(
1544+
self, mock_is_available, mock_fetch_scan_data, mock_get_project_info
1545+
):
1546+
self.client.login(username=self.base_user.username, password="secret")
1547+
action_url = reverse("api_v2:package-scan-summary", args=[self.package1.uuid])
1548+
mock_is_available.return_value = True
1549+
mock_get_project_info.return_value = {"uuid": "abcdef"}
1550+
mock_fetch_scan_data.return_value = {"summary": ""}
1551+
response = self.client.get(action_url)
1552+
self.assertEqual(200, response.status_code)
1553+
self.assertEqual({"summary": ""}, response.data)
1554+
1555+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.get_project_info")
1556+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_scan_data")
1557+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.is_available")
1558+
def test_api_package_viewset_scan_data_download_zip_action(
1559+
self, mock_is_available, mock_fetch_scan_data, mock_get_project_info
1560+
):
1561+
self.client.login(username=self.base_user.username, password="secret")
1562+
action_url = reverse("api_v2:package-scan-data-download-zip", args=[self.package1.uuid])
1563+
mock_is_available.return_value = True
1564+
mock_get_project_info.return_value = {"uuid": "abcdef"}
1565+
mock_fetch_scan_data.return_value = {}
1566+
response = self.client.get(action_url)
1567+
self.assertEqual(200, response.status_code)
1568+
self.assertEqual("application/zip", response["content-type"])
1569+
self.assertEqual(
1570+
'attachment; filename="package1.zip_scan.zip"', response["content-disposition"]
1571+
)
1572+
14781573
def test_api_package_protected_fields_as_read_only(self):
14791574
policy = UsagePolicy.objects.create(
14801575
label="PackagePolicy",

component_catalog/tests/test_scancodeio.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -113,23 +113,18 @@ def test_scancodeio_fetch_scan_info(self, mock_session_get):
113113

114114
scancodeio.fetch_scan_info(uri=uri)
115115
params = mock_session_get.call_args.kwargs["params"]
116-
expected = {"format": "json", "name__startswith": get_hash_uid(uri)}
117-
self.assertEqual(expected, params)
118-
119-
scancodeio.fetch_scan_info(
120-
uri=uri,
121-
user=self.basic_user,
122-
dataspace=self.basic_user.dataspace,
123-
)
124-
params = mock_session_get.call_args.kwargs["params"]
125116
expected = {
126-
"format": "json",
127117
"name__startswith": get_hash_uid(uri),
128118
"name__contains": get_hash_uid(self.basic_user.dataspace.uuid),
129-
"name__endswith": get_hash_uid(self.basic_user.uuid),
119+
"format": "json",
130120
}
131121
self.assertEqual(expected, params)
132122

123+
scancodeio.fetch_scan_info(uri=uri, user=self.basic_user)
124+
params = mock_session_get.call_args.kwargs["params"]
125+
expected["name__endswith"] = get_hash_uid(self.basic_user.uuid)
126+
self.assertEqual(expected, params)
127+
133128
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.request_get")
134129
def test_scancodeio_find_project(self, mock_request_get):
135130
scancodeio = ScanCodeIO(self.dataspace)
@@ -166,9 +161,9 @@ def test_scancodeio_find_project(self, mock_request_get):
166161
}
167162
self.assertIsNone(scancodeio.find_project(name="project_name"))
168163

169-
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.get_scan_results")
164+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.get_project_info")
170165
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_scan_data")
171-
def test_scancodeio_update_from_scan(self, mock_fetch_scan_data, mock_get_scan_results):
166+
def test_scancodeio_update_from_scan(self, mock_fetch_scan_data, mock_get_project_info):
172167
license_policy = make_usage_policy(self.dataspace, model=License)
173168
package_policy = make_usage_policy(self.dataspace, model=Package)
174169
make_associated_policy(license_policy, package_policy)
@@ -180,13 +175,13 @@ def test_scancodeio_update_from_scan(self, mock_fetch_scan_data, mock_get_scan_r
180175
self.package1.save()
181176
scancodeio = ScanCodeIO(self.dataspace)
182177

183-
mock_get_scan_results.return_value = None
178+
mock_get_project_info.return_value = None
184179
mock_fetch_scan_data.return_value = None
185180

186181
updated_fields = scancodeio.update_from_scan(self.package1, self.super_user)
187182
self.assertEqual([], updated_fields)
188183

189-
mock_get_scan_results.return_value = {"url": "https://scancode.io/"}
184+
mock_get_project_info.return_value = {"url": "https://scancode.io/"}
190185
updated_fields = scancodeio.update_from_scan(self.package1, self.super_user)
191186
self.assertEqual([], updated_fields)
192187

component_catalog/tests/test_views.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2971,7 +2971,9 @@ def test_delete_scan_view(self, mock_fetch_scan_list, mock_delete_scan):
29712971
self.assertEqual(404, response.status_code)
29722972

29732973
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_scan_data")
2974-
def test_send_scan_data_as_file_view(self, mock_fetch_scan_data):
2974+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.is_available")
2975+
def test_send_scan_data_as_file_view(self, mock_is_available, mock_fetch_scan_data):
2976+
mock_is_available.return_value = True
29752977
mock_fetch_scan_data.return_value = {}
29762978

29772979
project_uuid = "348df847-f48f-4ac7-b864-5785b44c65e2"

0 commit comments

Comments
 (0)