diff --git a/swh/web/api/views/vault.py b/swh/web/api/views/vault.py --- a/swh/web/api/views/vault.py +++ b/swh/web/api/views/vault.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015-2020 The Software Heritage developers +# Copyright (C) 2015-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information @@ -8,36 +8,38 @@ from django.http import HttpResponse from django.shortcuts import redirect -from swh.model import hashutil +from swh.model.identifiers import CoreSWHID, ObjectType from swh.web.api.apidoc import api_doc, format_docstring from swh.web.api.apiurls import api_route from swh.web.api.views.utils import api_lookup from swh.web.common import archive, query +from swh.web.common.exc import BadInputExc from swh.web.common.utils import reverse +###################################################### +# Common + +SWHID_RE = "swh:1:[a-z]{3}:[0-9a-z]{40}" + # XXX: a bit spaghetti. Would be better with class-based views. -def _dispatch_cook_progress(request, obj_type, obj_id): - hex_id = hashutil.hash_to_hex(obj_id) - object_name = obj_type.split("_")[0] +def _dispatch_cook_progress(request, bundle_type: str, swhid: CoreSWHID): if request.method == "GET": return api_lookup( archive.vault_progress, - obj_type, - obj_id, - notfound_msg=( - "Cooking of {} '{}' was never requested.".format(object_name, hex_id) - ), + bundle_type, + swhid, + notfound_msg=f"Cooking of {swhid} was never requested.", request=request, ) elif request.method == "POST": email = request.POST.get("email", request.GET.get("email", None)) return api_lookup( archive.vault_cook, - obj_type, - obj_id, + bundle_type, + swhid, email, - notfound_msg=("{} '{}' not found.".format(object_name.title(), hex_id)), + notfound_msg=f"{swhid} not found.", request=request, ) @@ -45,31 +47,32 @@ def _vault_response(vault_response: Dict[str, Any]) -> Dict[str, Any]: return { "fetch_url": vault_response["fetch_url"], - "obj_type": vault_response["type"], "progress_message": vault_response["progress_msg"], "id": vault_response["task_id"], "status": vault_response["task_status"], - "obj_id": vault_response["object_id"], + "swhid": vault_response["swhid"], } +###################################################### +# Flat bundles + + @api_route( - r"/vault/directory/(?P[0-9a-f]+)/", - "api-1-vault-cook-directory", + f"/vault/flat/(?P{SWHID_RE})/", + "api-1-vault-cook-flat", methods=["GET", "POST"], - checksum_args=["dir_id"], throttle_scope="swh_vault_cooking", never_cache=True, ) -@api_doc("/vault/directory/") +@api_doc("/vault/flat/") @format_docstring() -def api_vault_cook_directory(request, dir_id): +def api_vault_cook_flat(request, swhid): """ - .. http:get:: /api/1/vault/directory/(dir_id)/ - .. http:post:: /api/1/vault/directory/(dir_id)/ + .. http:get:: /api/1/vault/flat/(swhid)/ + .. http:post:: /api/1/vault/flat/(swhid)/ - Request the cooking of an archive for a directory or check - its cooking status. + Request the cooking of a simple archive, typically for a directory. That endpoint enables to create a vault cooking task for a directory through a POST request or check the status of a previously created one @@ -77,13 +80,13 @@ Once the cooking task has been executed, the resulting archive can be downloaded using the dedicated endpoint - :http:get:`/api/1/vault/directory/(dir_id)/raw/`. + :http:get:`/api/1/vault/flat/(swhid)/raw/`. Then to extract the cooked directory in the current one, use:: - $ tar xvf path/to/directory.tar.gz + $ tar xvf path/to/swh:1:*.tar.gz - :param string dir_id: the directory's sha1 identifier + :param string swhid: the object's SWHID :query string email: e-mail to notify when the archive is ready @@ -91,15 +94,13 @@ :>json string fetch_url: the url from which to download the archive once it has been cooked - (see :http:get:`/api/1/vault/directory/(dir_id)/raw/`) - :>json string obj_type: the type of object to cook - (directory or revision) + (see :http:get:`/api/1/vault/flat/(swhid)/raw/`) :>json string progress_message: message describing the cooking task progress :>json number id: the cooking task id :>json string status: the cooking task status (either **new**, **pending**, **done** or **failed**) - :>json string obj_id: the identifier of the object to cook + :>json string swhid: the identifier of the object to cook :statuscode 200: no error :statuscode 400: an invalid directory identifier has been provided @@ -107,72 +108,125 @@ request yet (in case of GET) or can not be found in the archive (in case of POST) """ + swhid = CoreSWHID.from_string(swhid) + if swhid.object_type == ObjectType.DIRECTORY: + res = _dispatch_cook_progress(request, "flat", swhid) + res["fetch_url"] = reverse( + "api-1-vault-fetch-flat", url_args={"swhid": str(swhid)}, request=request, + ) + return _vault_response(res) + elif swhid.object_type == ObjectType.CONTENT: + raise BadInputExc( + "Content objects do not need to be cooked, " + "use `/api/1/content/raw/` instead." + ) + elif swhid.object_type == ObjectType.REVISION: + # TODO: support revisions too? (the vault allows it) + raise BadInputExc( + "Only directories can be cooked as 'flat' bundles. " + "Use `/api/1/vault/gitfast/` to cook revisions, as gitfast bundles." + ) + else: + raise BadInputExc("Only directories can be cooked as 'flat' bundles.") + + +@api_route( + r"/vault/directory/(?P[0-9a-f]+)/", + "api-1-vault-cook-directory", + methods=["GET", "POST"], + checksum_args=["dir_id"], + throttle_scope="swh_vault_cooking", + never_cache=True, +) +@api_doc("/vault/directory/", tags=["hidden"]) +@format_docstring() +def api_vault_cook_directory(request, dir_id): + """ + Replaced by :http:get:`/api/1/vault/flat/(swhid)/` + """ _, obj_id = query.parse_hash_with_algorithms_or_throws( dir_id, ["sha1"], "Only sha1_git is supported." ) - res = _dispatch_cook_progress(request, "directory", obj_id) + swhid = f"swh:1:dir:{obj_id.hex()}" + res = _dispatch_cook_progress(request, "flat", CoreSWHID.from_string(swhid)) res["fetch_url"] = reverse( - "api-1-vault-fetch-directory", url_args={"dir_id": dir_id}, request=request, + "api-1-vault-fetch-flat", url_args={"swhid": swhid}, request=request, ) return _vault_response(res) @api_route( - r"/vault/directory/(?P[0-9a-f]+)/raw/", - "api-1-vault-fetch-directory", - checksum_args=["dir_id"], + f"/vault/flat/(?P{SWHID_RE})/raw/", "api-1-vault-fetch-flat", ) @api_doc("/vault/directory/raw/") -def api_vault_fetch_directory(request, dir_id): +def api_vault_fetch_flat(request, swhid): """ - .. http:get:: /api/1/vault/directory/(dir_id)/raw/ + .. http:get:: /api/1/vault/flat/(swhid)/raw/ - Fetch the cooked archive for a directory. + Fetch the cooked archive for a flat bundle. - See :http:get:`/api/1/vault/directory/(dir_id)/` to get more - details on directory cooking. + See :http:get:`/api/1/vault/flat/(swhid)/` to get more + details on 'flat' bundle cooking. - :param string dir_id: the directory's sha1 identifier + :param string swhid: the SWHID of the object to cook :resheader Content-Type: application/octet-stream :statuscode 200: no error - :statuscode 400: an invalid directory identifier has been provided :statuscode 404: requested directory did not receive any cooking request yet (in case of GET) or can not be found in the archive (in case of POST) """ - _, obj_id = query.parse_hash_with_algorithms_or_throws( - dir_id, ["sha1"], "Only sha1_git is supported." - ) res = api_lookup( archive.vault_fetch, - "directory", - obj_id, - notfound_msg="Cooked archive for directory '{}' not found.".format(dir_id), + "flat", + CoreSWHID.from_string(swhid), + notfound_msg=f"Cooked archive for {swhid} not found.", request=request, ) - fname = "{}.tar.gz".format(dir_id) + fname = "{}.tar.gz".format(swhid) response = HttpResponse(res, content_type="application/gzip") response["Content-disposition"] = "attachment; filename={}".format(fname) return response @api_route( - r"/vault/revision/(?P[0-9a-f]+)/gitfast/", - "api-1-vault-cook-revision_gitfast", + r"/vault/directory/(?P[0-9a-f]+)/raw/", + "api-1-vault-fetch-directory", + checksum_args=["dir_id"], +) +@api_doc("/vault/directory/raw/", tags=["hidden"]) +def api_vault_fetch_directory(request, dir_id): + """ + Replaced by :http:get:`/api/1/vault/flat/raw/` + """ + _, obj_id = query.parse_hash_with_algorithms_or_throws( + dir_id, ["sha1"], "Only sha1_git is supported." + ) + rev_flat_raw_url = reverse( + "api-1-vault-fetch-flat", url_args={"swhid": f"swh:1:dir:{dir_id}"} + ) + return redirect(rev_flat_raw_url) + + +###################################################### +# gitfast bundles + + +@api_route( + f"/vault/gitfast/(?P{SWHID_RE})/", + "api-1-vault-cook-gitfast", methods=["GET", "POST"], - checksum_args=["rev_id"], throttle_scope="swh_vault_cooking", never_cache=True, ) -@api_doc("/vault/revision/gitfast/") +@api_doc("/vault/gitfast/", tags=["hidden"]) @format_docstring() -def api_vault_cook_revision_gitfast(request, rev_id): +def api_vault_cook_gitfast(request, swhid): """ - .. http:get:: /api/1/vault/revision/(rev_id)/gitfast/ - .. http:post:: /api/1/vault/revision/(rev_id)/gitfast/ + .. http:get:: /api/1/vault/gitfast/(swhid)/ + .. http:post:: /api/1/vault/gitfast/(swhid)/ Request the cooking of a gitfast archive for a revision or check its cooking status. @@ -188,7 +242,7 @@ Then to import the revision in the current directory, use:: $ git init - $ zcat path/to/revision.gitfast.gz | git fast-import + $ zcat path/to/swh:1:rev:*.gitfast.gz | git fast-import $ git checkout HEAD :param string rev_id: the revision's sha1 identifier @@ -199,88 +253,113 @@ :>json string fetch_url: the url from which to download the archive once it has been cooked - (see :http:get:`/api/1/vault/revision/(rev_id)/gitfast/raw/`) - :>json string obj_type: the type of object to cook - (directory or revision) + (see :http:get:`/api/1/vault/gitfast/(rev_id)/raw/`) :>json string progress_message: message describing the cooking task progress :>json number id: the cooking task id :>json string status: the cooking task status (new/pending/done/failed) - :>json string obj_id: the identifier of the object to cook + :>json string swhid: the identifier of the object to cook :statuscode 200: no error - :statuscode 400: an invalid revision identifier has been provided :statuscode 404: requested directory did not receive any cooking request yet (in case of GET) or can not be found in the archive (in case of POST) """ + swhid = CoreSWHID.from_string(swhid) + if swhid.object_type == ObjectType.REVISION: + res = _dispatch_cook_progress(request, "gitfast", swhid) + res["fetch_url"] = reverse( + "api-1-vault-fetch-gitfast", + url_args={"swhid": str(swhid)}, + request=request, + ) + return _vault_response(res) + elif swhid.object_type == ObjectType.CONTENT: + raise BadInputExc( + "Content objects do not need to be cooked, " + "use `/api/1/content/raw/` instead." + ) + elif swhid.object_type == ObjectType.DIRECTORY: + raise BadInputExc( + "Only revisions can be cooked as 'gitfast' bundles. " + "Use `/api/1/vault/flat/` to cook directories, as flat bundles." + ) + else: + raise BadInputExc("Only revisions can be cooked as 'gitfast' bundles.") + + +@api_route( + r"/vault/revision/(?P[0-9a-f]+)/gitfast/", + "api-1-vault-cook-revision_gitfast", + methods=["GET", "POST"], + checksum_args=["rev_id"], + throttle_scope="swh_vault_cooking", + never_cache=True, +) +@api_doc("/vault/revision/gitfast/", tags=["hidden"]) +@format_docstring() +def api_vault_cook_revision_gitfast(request, rev_id): + """ + Replaced by :http:get:`/api/1/vault/gitfast/` + """ _, obj_id = query.parse_hash_with_algorithms_or_throws( rev_id, ["sha1"], "Only sha1_git is supported." ) - res = _dispatch_cook_progress(request, "revision_gitfast", obj_id) + swhid = f"swh:1:rev:{obj_id.hex()}" + res = _dispatch_cook_progress(request, "gitfast", CoreSWHID.from_string(swhid)) res["fetch_url"] = reverse( - "api-1-vault-fetch-revision_gitfast", - url_args={"rev_id": rev_id}, - request=request, + "api-1-vault-fetch-gitfast", url_args={"swhid": swhid}, request=request, ) return _vault_response(res) @api_route( - r"/vault/revision/(?P[0-9a-f]+)/gitfast/raw/", - "api-1-vault-fetch-revision_gitfast", - checksum_args=["rev_id"], + f"/vault/gitfast/(?P{SWHID_RE})/raw/", "api-1-vault-fetch-gitfast", ) -@api_doc("/vault/revision/gitfast/raw/") -def api_vault_fetch_revision_gitfast(request, rev_id): +@api_doc("/vault/gitfast/raw/") +def api_vault_fetch_revision_gitfast(request, swhid): """ - .. http:get:: /api/1/vault/revision/(rev_id)/gitfast/raw/ + .. http:get:: /api/1/vault/gitfast/(swhid)/raw/ Fetch the cooked gitfast archive for a revision. - See :http:get:`/api/1/vault/revision/(rev_id)/gitfast/` to get more - details on directory cooking. + See :http:get:`/api/1/vault/gitfast/(swhid)/` to get more + details on gitfast cooking. :param string rev_id: the revision's sha1 identifier :resheader Content-Type: application/octet-stream :statuscode 200: no error - :statuscode 400: an invalid revision identifier has been provided :statuscode 404: requested directory did not receive any cooking request yet (in case of GET) or can not be found in the archive (in case of POST) """ - _, obj_id = query.parse_hash_with_algorithms_or_throws( - rev_id, ["sha1"], "Only sha1_git is supported." - ) res = api_lookup( archive.vault_fetch, - "revision_gitfast", - obj_id, - notfound_msg="Cooked archive for revision '{}' not found.".format(rev_id), + "gitfast", + CoreSWHID.from_string(swhid), + notfound_msg="Cooked archive for {} not found.".format(swhid), request=request, ) - fname = "{}.gitfast.gz".format(rev_id) + fname = "{}.gitfast.gz".format(swhid) response = HttpResponse(res, content_type="application/gzip") response["Content-disposition"] = "attachment; filename={}".format(fname) return response @api_route( - r"/vault/revision_gitfast/(?P[0-9a-f]+)/raw/", - "api-1-vault-revision_gitfast-raw", + r"/vault/revision/(?P[0-9a-f]+)/gitfast/raw/", + "api-1-vault-fetch-revision_gitfast", checksum_args=["rev_id"], ) @api_doc("/vault/revision_gitfast/raw/", tags=["hidden"]) def _api_vault_revision_gitfast_raw(request, rev_id): """ - The vault backend sends an email containing an invalid url to fetch a - gitfast archive. So setup a redirection to the correct one as a temporary - workaround. + Replaced by :http:get:`/api/1/vault/gitfast/raw/` """ rev_gitfast_raw_url = reverse( - "api-1-vault-fetch-revision_gitfast", url_args={"rev_id": rev_id} + "api-1-vault-fetch-gitfast", url_args={"swhid": f"swh:1:rev:{rev_id}"} ) return redirect(rev_gitfast_raw_url) diff --git a/swh/web/common/archive.py b/swh/web/common/archive.py --- a/swh/web/common/archive.py +++ b/swh/web/common/archive.py @@ -11,7 +11,14 @@ from urllib.parse import urlparse from swh.model import hashutil -from swh.model.identifiers import CONTENT, DIRECTORY, RELEASE, REVISION, SNAPSHOT +from swh.model.identifiers import ( + CONTENT, + DIRECTORY, + RELEASE, + REVISION, + SNAPSHOT, + CoreSWHID, +) from swh.model.model import OriginVisit, Revision from swh.storage.algos import diff, revisions_walker from swh.storage.algos.origin import origin_get_latest_visit_status @@ -1234,29 +1241,29 @@ return (rev["id"], lookup_directory_with_revision(rev["id"], path, with_data)) -def _vault_request(vault_fn, *args, **kwargs): +def _vault_request(vault_fn, bundle_type: str, swhid: CoreSWHID, **kwargs): try: - return vault_fn(*args, **kwargs) + return vault_fn(bundle_type, swhid, **kwargs) except VaultNotFoundExc: return None -def vault_cook(obj_type, obj_id, email=None): +def vault_cook(bundle_type: str, swhid: CoreSWHID, email=None): """Cook a vault bundle. """ - return _vault_request(vault.cook, obj_type, obj_id, email=email) + return _vault_request(vault.cook, bundle_type, swhid, email=email) -def vault_fetch(obj_type, obj_id): +def vault_fetch(bundle_type: str, swhid: CoreSWHID): """Fetch a vault bundle. """ - return _vault_request(vault.fetch, obj_type, obj_id) + return _vault_request(vault.fetch, bundle_type, swhid) -def vault_progress(obj_type, obj_id): +def vault_progress(bundle_type: str, swhid: CoreSWHID): """Get the current progress of a vault bundle. """ - return _vault_request(vault.progress, obj_type, obj_id) + return _vault_request(vault.progress, bundle_type, swhid) def diff_revision(rev_id): diff --git a/swh/web/tests/api/views/test_vault.py b/swh/web/tests/api/views/test_vault.py --- a/swh/web/tests/api/views/test_vault.py +++ b/swh/web/tests/api/views/test_vault.py @@ -1,11 +1,14 @@ -# Copyright (C) 2017-2020 The Software Heritage developers +# Copyright (C) 2017-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information +import re + from hypothesis import given +import pytest -from swh.model import hashutil +from swh.model.identifiers import CoreSWHID from swh.vault.exc import NotFoundExc from swh.web.common.utils import reverse from swh.web.tests.strategies import ( @@ -21,25 +24,183 @@ check_http_post_response, ) +##################### +# Current API: + @given(directory(), revision()) def test_api_vault_cook(api_client, mocker, directory, revision): mock_archive = mocker.patch("swh.web.api.views.vault.archive") - for obj_type, obj_id in ( - ("directory", directory), - ("revision_gitfast", revision), + for bundle_type, swhid in ( + ("flat", f"swh:1:dir:{directory}"), + ("gitfast", f"swh:1:rev:{revision}"), ): fetch_url = reverse( - f"api-1-vault-fetch-{obj_type}", url_args={f"{obj_type[:3]}_id": obj_id}, + f"api-1-vault-fetch-{bundle_type}", url_args={"swhid": swhid}, + ) + stub_cook = { + "type": bundle_type, + "progress_msg": None, + "task_id": 1, + "task_status": "done", + "swhid": swhid, + } + stub_fetch = b"content" + + mock_archive.vault_cook.return_value = stub_cook + mock_archive.vault_fetch.return_value = stub_fetch + + email = "test@test.mail" + url = reverse( + f"api-1-vault-cook-{bundle_type}", + url_args={"swhid": swhid}, + query_params={"email": email}, + ) + + rv = check_api_post_responses(api_client, url, data=None, status_code=200) + assert rv.data == { + "fetch_url": rv.wsgi_request.build_absolute_uri(fetch_url), + "progress_message": None, + "id": 1, + "status": "done", + "swhid": swhid, + } + mock_archive.vault_cook.assert_called_with( + bundle_type, CoreSWHID.from_string(swhid), email + ) + + rv = check_http_get_response(api_client, fetch_url, status_code=200) + assert rv["Content-Type"] == "application/gzip" + assert rv.content == stub_fetch + mock_archive.vault_fetch.assert_called_with( + bundle_type, CoreSWHID.from_string(swhid) + ) + + +@given(directory(), revision(), unknown_directory(), unknown_revision()) +def test_api_vault_cook_notfound( + api_client, mocker, directory, revision, unknown_directory, unknown_revision +): + mock_vault = mocker.patch("swh.web.common.archive.vault") + mock_vault.cook.side_effect = NotFoundExc("object not found") + mock_vault.fetch.side_effect = NotFoundExc("cooked archive not found") + mock_vault.progress.side_effect = NotFoundExc("cooking request not found") + + for bundle_type, swhid in ( + ("flat", f"swh:1:dir:{directory}"), + ("gitfast", f"swh:1:rev:{revision}"), + ): + + url = reverse(f"api-1-vault-cook-{bundle_type}", url_args={"swhid": swhid}) + + rv = check_api_get_responses(api_client, url, status_code=404) + + assert rv.data["exception"] == "NotFoundExc" + assert rv.data["reason"] == f"Cooking of {swhid} was never requested." + mock_vault.progress.assert_called_with( + bundle_type, CoreSWHID.from_string(swhid) + ) + + for bundle_type, swhid in ( + ("flat", f"swh:1:dir:{unknown_directory}"), + ("gitfast", f"swh:1:rev:{unknown_revision}"), + ): + url = reverse(f"api-1-vault-cook-{bundle_type}", url_args={"swhid": swhid}) + rv = check_api_post_responses(api_client, url, data=None, status_code=404) + + assert rv.data["exception"] == "NotFoundExc" + assert rv.data["reason"] == f"{swhid} not found." + mock_vault.cook.assert_called_with( + bundle_type, CoreSWHID.from_string(swhid), email=None + ) + + fetch_url = reverse( + f"api-1-vault-fetch-{bundle_type}", url_args={"swhid": swhid}, + ) + + rv = check_api_get_responses(api_client, fetch_url, status_code=404) + assert rv.data["exception"] == "NotFoundExc" + assert rv.data["reason"] == f"Cooked archive for {swhid} not found." + mock_vault.fetch.assert_called_with(bundle_type, CoreSWHID.from_string(swhid)) + + +@pytest.mark.parametrize("bundle_type", ["flat", "gitfast"]) +def test_api_vault_cook_error_content(api_client, mocker, bundle_type): + swhid = "swh:1:cnt:" + "0" * 40 + + email = "test@test.mail" + url = reverse( + f"api-1-vault-cook-{bundle_type}", + url_args={"swhid": swhid}, + query_params={"email": email}, + ) + + rv = check_api_post_responses(api_client, url, data=None, status_code=400) + assert rv.data == { + "exception": "BadInputExc", + "reason": ( + "Content objects do not need to be cooked, " + "use `/api/1/content/raw/` instead." + ), + } + + +@pytest.mark.parametrize( + "bundle_type,swhid_type,hint", + [ + ("flat", "rev", True), + ("flat", "rel", False), + ("flat", "snp", False), + ("gitfast", "dir", True), + ("gitfast", "rel", False), + ("gitfast", "snp", False), + ], +) +def test_api_vault_cook_error(api_client, mocker, bundle_type, swhid_type, hint): + swhid = f"swh:1:{swhid_type}:" + "0" * 40 + + email = "test@test.mail" + url = reverse( + f"api-1-vault-cook-{bundle_type}", + url_args={"swhid": swhid}, + query_params={"email": email}, + ) + + rv = check_api_post_responses(api_client, url, data=None, status_code=400) + assert rv.data["exception"] == "BadInputExc" + if hint: + assert re.match( + r"Only .* can be cooked as .* bundles\. Use .*", rv.data["reason"] + ) + else: + assert re.match(r"Only .* can be cooked as .* bundles\.", rv.data["reason"]) + + +##################### +# Legacy API: + + +@given(directory(), revision()) +def test_api_vault_cook_legacy(api_client, mocker, directory, revision): + mock_archive = mocker.patch("swh.web.api.views.vault.archive") + + for obj_type, bundle_type, obj_id in ( + ("directory", "flat", directory), + ("revision_gitfast", "gitfast", revision), + ): + swhid = f"swh:1:{obj_type[:3]}:{obj_id}" + + fetch_url = reverse( + f"api-1-vault-fetch-{bundle_type}", url_args={"swhid": swhid}, ) stub_cook = { "type": obj_type, "progress_msg": None, "task_id": 1, "task_status": "done", - "object_id": obj_id, + "swhid": swhid, } stub_fetch = b"content" @@ -56,26 +217,25 @@ rv = check_api_post_responses(api_client, url, data=None, status_code=200) assert rv.data == { "fetch_url": rv.wsgi_request.build_absolute_uri(fetch_url), - "obj_type": obj_type, "progress_message": None, "id": 1, "status": "done", - "obj_id": obj_id, + "swhid": swhid, } mock_archive.vault_cook.assert_called_with( - obj_type, hashutil.hash_to_bytes(obj_id), email + bundle_type, CoreSWHID.from_string(swhid), email ) rv = check_http_get_response(api_client, fetch_url, status_code=200) assert rv["Content-Type"] == "application/gzip" assert rv.content == stub_fetch mock_archive.vault_fetch.assert_called_with( - obj_type, hashutil.hash_to_bytes(obj_id) + bundle_type, CoreSWHID.from_string(swhid) ) @given(directory(), revision()) -def test_api_vault_cook_uppercase_hash(api_client, directory, revision): +def test_api_vault_cook_uppercase_hash_legacy(api_client, directory, revision): for obj_type, obj_id in ( ("directory", directory), @@ -111,7 +271,7 @@ @given(directory(), revision(), unknown_directory(), unknown_revision()) -def test_api_vault_cook_notfound( +def test_api_vault_cook_notfound_legacy( api_client, mocker, directory, revision, unknown_directory, unknown_revision ): mock_vault = mocker.patch("swh.web.common.archive.vault") @@ -119,31 +279,30 @@ mock_vault.fetch.side_effect = NotFoundExc("cooked archive not found") mock_vault.progress.side_effect = NotFoundExc("cooking request not found") - for obj_type, obj_id in ( - ("directory", directory), - ("revision_gitfast", revision), + for obj_type, bundle_type, obj_id in ( + ("directory", "flat", directory), + ("revision_gitfast", "gitfast", revision), ): - obj_name = obj_type.split("_")[0] - url = reverse( f"api-1-vault-cook-{obj_type}", url_args={f"{obj_type[:3]}_id": obj_id}, ) + swhid = f"swh:1:{obj_type[:3]}:{obj_id}" + rv = check_api_get_responses(api_client, url, status_code=404) assert rv.data["exception"] == "NotFoundExc" - assert ( - rv.data["reason"] - == f"Cooking of {obj_name} '{obj_id}' was never requested." + assert rv.data["reason"] == f"Cooking of {swhid} was never requested." + mock_vault.progress.assert_called_with( + bundle_type, CoreSWHID.from_string(swhid) ) - mock_vault.progress.assert_called_with(obj_type, hashutil.hash_to_bytes(obj_id)) - for obj_type, obj_id in ( - ("directory", unknown_directory), - ("revision_gitfast", unknown_revision), + for obj_type, bundle_type, obj_id in ( + ("directory", "flat", unknown_directory), + ("revision_gitfast", "gitfast", unknown_revision), ): - obj_name = obj_type.split("_")[0] + swhid = f"swh:1:{obj_type[:3]}:{obj_id}" url = reverse( f"api-1-vault-cook-{obj_type}", url_args={f"{obj_type[:3]}_id": obj_id} @@ -151,18 +310,23 @@ rv = check_api_post_responses(api_client, url, data=None, status_code=404) assert rv.data["exception"] == "NotFoundExc" - assert rv.data["reason"] == f"{obj_name.title()} '{obj_id}' not found." + assert rv.data["reason"] == f"{swhid} not found." mock_vault.cook.assert_called_with( - obj_type, hashutil.hash_to_bytes(obj_id), email=None + bundle_type, CoreSWHID.from_string(swhid), email=None ) fetch_url = reverse( f"api-1-vault-fetch-{obj_type}", url_args={f"{obj_type[:3]}_id": obj_id}, ) - rv = check_api_get_responses(api_client, fetch_url, status_code=404) - assert rv.data["exception"] == "NotFoundExc" - assert ( - rv.data["reason"] == f"Cooked archive for {obj_name} '{obj_id}' not found." + # Redirected to the current 'fetch' url + rv = check_http_get_response(api_client, fetch_url, status_code=302) + redirect_url = reverse( + f"api-1-vault-fetch-{bundle_type}", url_args={"swhid": swhid}, ) - mock_vault.fetch.assert_called_with(obj_type, hashutil.hash_to_bytes(obj_id)) + assert rv["location"] == redirect_url + + rv = check_api_get_responses(api_client, redirect_url, status_code=404) + assert rv.data["exception"] == "NotFoundExc" + assert rv.data["reason"] == f"Cooked archive for {swhid} not found." + mock_vault.fetch.assert_called_with(bundle_type, CoreSWHID.from_string(swhid))