# Copyright © The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""Tests for the asset views."""

import base64
from typing import Any

from django.urls import reverse
from django.utils.dateparse import parse_datetime
from rest_framework import status

from debusine.artifacts.models import CollectionCategory
from debusine.assets import AssetCategory, KeyPurpose, SigningKeyData
from debusine.db.models import (
    Artifact,
    Asset,
    AssetUsage,
    Collection,
    Scope,
    Token,
    WorkRequest,
    Worker,
    Workspace,
)
from debusine.db.models.assets import AssetUsageRoles
from debusine.db.models.scopes import ScopeRoles
from debusine.db.permissioncontext import PermissionContext
from debusine.db.playground import scenarios
from debusine.db.playground.playground import SourceUploadArtifacts
from debusine.server.views.assets import AssetPermissionCheckView, log
from debusine.signing.models import SigningMode
from debusine.signing.tasks.models import (
    DebsignData,
    DebsignDynamicData,
    SignData,
    SignDynamicData,
    SignRepositoryIndexData,
    SignRepositoryIndexDynamicData,
)
from debusine.tasks.models import LookupMultiple, WorkerType
from debusine.test.django import TestCase, TestResponseType


class AssetViewTests(TestCase):
    """Tests for AssetView."""

    scenario = scenarios.DefaultContextAPI()
    playground_memory_file_store = False

    def post_asset_create(
        self,
        *,
        token_key: str,
        data: dict[str, Any],
        scope: Scope,
    ) -> TestResponseType:
        """
        Call self.client.post() to api:asset.

        Pass the token.key and content_type="application/json".

        :param token_key: value of the Token header for the request
        :param data: data to post
        """
        headers: dict[str, str] = {}
        headers["X-Debusine-Scope"] = scope.name

        return self.client.post(
            reverse("api:asset"),
            data=data,
            HTTP_TOKEN=token_key,
            content_type="application/json",
            headers=headers,
        )

    def get_asset_list(
        self,
        *,
        token_key: str,
        query_params: dict[str, Any],
        scope: Scope,
    ) -> TestResponseType:
        """
        Call self.client.get() to api:asset.

        Pass the token.key and content_type="application/json".

        :param token_key: value of the Token header for the request
        :param query_params: query keys and values
        """
        headers: dict[str, str] = {}
        headers["Accept"] = "application/json"
        headers["X-Debusine-Scope"] = scope.name

        return self.client.get(
            reverse("api:asset"),
            HTTP_TOKEN=token_key,
            headers=headers,
            data=query_params,
        )

    def signing_key_asset_as_dict_request(
        self,
        fingerprint: str,
        public_key: str,
        workspace: Workspace,
        description: str | None = None,
        purpose: KeyPurpose = KeyPurpose.OPENPGP_UPLOAD,
        work_request_id: int | None = None,
    ) -> dict[str, Any]:
        """Generate an asset creation request dict."""
        return {
            "workspace": workspace.name,
            "category": AssetCategory.SIGNING_KEY,
            "work_request": work_request_id,
            "data": SigningKeyData(
                purpose=purpose,
                fingerprint=fingerprint,
                public_key=base64.b64encode(public_key.encode()).decode(),
                description=description,
            ).model_dump(),
        }

    def test_create_asset(self) -> None:
        """Create a signing-key asset."""
        workspace = self.playground.get_default_workspace()
        user = self.playground.get_default_user()
        self.playground.create_group_role(
            workspace.scope, ScopeRoles.OWNER, users=[user]
        )
        response = self.post_asset_create(
            token_key=self.scenario.user_token.key,
            data=self.signing_key_asset_as_dict_request(
                fingerprint="ABC123",
                public_key="PUBLIC KEY",
                workspace=workspace,
            ),
            scope=workspace.scope,
        )
        body = self.assertAPIResponseOk(response, status.HTTP_201_CREATED)
        asset = Asset.objects.get(id=body["id"])
        self.assertEqual(asset.category, AssetCategory.SIGNING_KEY)
        data = asset.data_model
        assert isinstance(data, SigningKeyData)
        self.assertEqual(data.fingerprint, "ABC123")
        self.assertEqual(data.public_key, b"PUBLIC KEY")
        self.assertEqual(asset.workspace, workspace)
        self.assertEqual(asset.created_by, user)
        self.assertIsNone(asset.created_by_work_request)
        self.assertTrue(asset.usage.filter(workspace=workspace).exists())

    def test_worker_create_asset(self) -> None:
        """Worker creates a signing-key asset."""
        workspace = self.playground.get_default_workspace()
        user = self.playground.get_default_user()
        self.playground.create_group_role(
            workspace.scope, ScopeRoles.OWNER, users=[user]
        )
        worker = self.playground.create_worker(worker_type=WorkerType.SIGNING)
        work_request = self.playground.create_worker_task(
            worker=worker,
            mark_running=True,
            workspace=workspace,
            created_by=user,
        )
        token = self.playground.create_work_request_token(work_request)
        response = self.post_asset_create(
            token_key=token.key,
            data=self.signing_key_asset_as_dict_request(
                fingerprint="ABC123",
                public_key="PUBLIC KEY",
                workspace=workspace,
                work_request_id=work_request.id,
            ),
            scope=workspace.scope,
        )
        body = self.assertAPIResponseOk(response, status.HTTP_201_CREATED)
        asset = Asset.objects.get(id=body["id"])
        self.assertEqual(asset.category, AssetCategory.SIGNING_KEY)
        data = asset.data_model
        assert isinstance(data, SigningKeyData)
        self.assertEqual(data.fingerprint, "ABC123")
        self.assertEqual(data.public_key, b"PUBLIC KEY")
        self.assertEqual(asset.workspace, workspace)
        self.assertIsNone(asset.created_by)
        self.assertEqual(asset.created_by_work_request, work_request)
        self.assertTrue(asset.usage.filter(workspace=workspace).exists())

    def test_create_asset_idempotent(self) -> None:
        """Create a signing-key asset that already exists, return 200."""
        workspace = self.playground.get_default_workspace()
        user = self.playground.get_default_user()
        self.playground.create_group_role(
            workspace.scope, ScopeRoles.OWNER, users=[user]
        )
        asset = self.playground.create_signing_key_asset()
        data = asset.data_model
        assert isinstance(data, SigningKeyData)
        response = self.post_asset_create(
            token_key=self.scenario.user_token.key,
            data=self.signing_key_asset_as_dict_request(
                purpose=data.purpose,
                fingerprint=data.fingerprint,
                public_key="PUBLIC KEY",
                workspace=workspace,
            ),
            scope=workspace.scope,
        )
        body = self.assertAPIResponseOk(response)
        self.assertEqual(body["id"], asset.id)

    def assert_asset_equals(
        self, asset: Asset, asset_response: dict[str, Any]
    ) -> None:
        """Assert that asset == asset_response."""
        # Not true in general, but true for all the assets that the view can
        # currently return (since it uses Asset.objects.in_current_scope).
        assert asset.workspace is not None

        self.assertEqual(asset_response["id"], asset.id)
        self.assertEqual(asset_response["data"], asset.data)
        self.assertEqual(asset_response["category"], asset.category)
        self.assertEqual(asset_response["scope"], asset.workspace.scope.name)
        self.assertEqual(asset_response["workspace"], asset.workspace.name)
        self.assertEqual(
            parse_datetime(asset_response["created_at"]), asset.created_at
        )
        self.assertEqual(
            asset_response["work_request"], asset.created_by_work_request_id
        )

    def test_list_asset_by_id(self) -> None:
        """List an asset by ID."""
        workspace = self.playground.get_default_workspace()
        asset = self.playground.create_signing_key_asset()
        response = self.get_asset_list(
            token_key=self.scenario.user_token.key,
            query_params={"asset": asset.id},
            scope=workspace.scope,
        )
        body = self.assertAPIResponseOk(response)
        self.assert_asset_equals(asset, body[0])

    def test_list_asset_by_work_request(self) -> None:
        """List an asset by work request."""
        workspace = self.playground.get_default_workspace()
        work_request = self.playground.create_worker_task()
        asset = self.playground.create_signing_key_asset(
            created_by_work_request=work_request
        )
        response = self.get_asset_list(
            token_key=self.scenario.user_token.key,
            query_params={"work_request": work_request.id},
            scope=workspace.scope,
        )
        body = self.assertAPIResponseOk(response)
        self.assert_asset_equals(asset, body[0])

    def test_list_assets_by_workspace(self) -> None:
        """List assets by workspace."""
        workspace = self.playground.get_default_workspace()
        asset1 = self.playground.create_signing_key_asset()
        asset2 = self.playground.create_signing_key_asset(
            purpose=KeyPurpose.UEFI
        )
        workspace2 = self.playground.create_workspace(name="workspace2")
        self.playground.create_signing_key_asset(workspace=workspace2)
        response = self.get_asset_list(
            token_key=self.scenario.user_token.key,
            query_params={"workspace": workspace.name},
            scope=workspace.scope,
        )
        body = self.assertAPIResponseOk(response)
        self.assertEqual(len(body), 2)
        self.assert_asset_equals(asset1, body[0])
        self.assert_asset_equals(asset2, body[1])

    def test_list_assets_unfiltered(self) -> None:
        """List assets without a filter."""
        workspace = self.playground.get_default_workspace()
        response = self.get_asset_list(
            token_key=self.scenario.user_token.key,
            query_params={},
            scope=workspace.scope,
        )
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        body = response.json()
        self.assertEqual(
            body,
            {
                "title": (
                    '"asset", "work_request", or "workspace" parameters are '
                    'mandatory'
                ),
            },
        )


class AssetPermissionCheckViewTests(TestCase):
    """Tests for AssetPermissionCheckView."""

    playground_memory_file_store = False
    worker: Worker

    def setUp(self) -> None:
        super().setUp()
        self.worker = self.playground.create_worker(
            worker_type=WorkerType.SIGNING
        )

    def post_permission_check(
        self,
        *,
        asset_category: str,
        asset_slug: str,
        permission_name: str,
        data: dict[str, Any],
        token: Token | None = None,
    ) -> TestResponseType:
        """
        Call self.client.post() to api:asset-permission-check.

        Pass the token.key and content_type="application/json".

        :param token_key: value of the Token header for the request
        :param data: data to post
        """
        if token is None:
            work_request = self.playground.create_worker_task(
                worker=self.worker, mark_running=True
            )
            token = self.playground.create_work_request_token(work_request)
        return self.client.post(
            reverse(
                "api:asset-permission-check",
                kwargs={
                    "asset_category": asset_category,
                    "asset_slug": asset_slug,
                    "permission_name": permission_name,
                },
            ),
            data=data,
            HTTP_TOKEN=token.key,
            content_type="application/json",
        )

    def create_debsign_work_request(
        self,
        asset: Asset,
        upload_artifacts: SourceUploadArtifacts,
        **kwargs: Any,
    ) -> WorkRequest:
        """Create a debsign WorkRequest using asset and upload_artifacts."""
        asset_model = asset.data_model
        assert isinstance(asset_model, SigningKeyData)
        wr = self.playground.create_signing_task(
            task_name="debsign",
            task_data=DebsignData(
                unsigned=upload_artifacts.upload.id,
                key=asset_model.fingerprint,
            ),
            mark_running=True,
            **kwargs,
        )
        wr.dynamic_task_data = DebsignDynamicData(
            unsigned_id=upload_artifacts.upload.id
        ).model_dump()
        wr.save()
        return wr

    def test_permission_check_on_upload(self) -> None:
        workspace = self.playground.get_default_workspace()
        asset = self.playground.create_signing_key_asset()
        upload_artifacts = self.playground.create_upload_artifacts(binary=False)
        work_request = self.create_debsign_work_request(asset, upload_artifacts)
        token = self.playground.create_work_request_token(work_request)
        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug=asset.slug,
            permission_name="sign_with",
            token=token,
            data={
                "artifact_id": upload_artifacts.upload.id,
                "work_request_id": work_request.id,
                "workspace": workspace.name,
            },
        )
        body = self.assertAPIResponseOk(response)
        self.assertFalse(body["has_permission"])
        self.assertEqual(body["username"], work_request.created_by.username)
        self.assertEqual(body["user_id"], work_request.created_by.id)
        self.assertEqual(
            body["resource"],
            {
                "architecture": "source",
                "source": "hello",
                "version": "1.0-1",
                "distribution": "unstable",
            },
        )

    def test_permission_check_on_upload_granted(self) -> None:
        workspace = self.playground.get_default_workspace()
        user = self.playground.get_default_user()
        asset = self.playground.create_signing_key_asset()
        asset_usage = self.playground.create_asset_usage(
            resource=asset, workspace=workspace
        )
        self.playground.create_group_role(
            asset_usage, AssetUsageRoles.SIGNER, users=[user]
        )
        upload_artifacts = self.playground.create_upload_artifacts(binary=False)
        work_request = self.create_debsign_work_request(
            asset, upload_artifacts, created_by=user
        )
        token = self.playground.create_work_request_token(work_request)
        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug=asset.slug,
            permission_name="sign_with",
            token=token,
            data={
                "artifact_id": upload_artifacts.upload.id,
                "work_request_id": work_request.id,
                "workspace": workspace.name,
            },
        )
        body = self.assertAPIResponseOk(response)
        self.assertTrue(body["has_permission"])
        self.assertEqual(body["username"], user.username)
        self.assertEqual(body["user_id"], work_request.created_by.id)

    def create_sign_work_request(
        self,
        asset: Asset,
        signing_input_artifacts: list[Artifact],
        **kwargs: Any,
    ) -> WorkRequest:
        """Create a sign WorkRequest using asset and signing_input."""
        asset_model = asset.data_model
        assert isinstance(asset_model, SigningKeyData)
        signing_input_ids = [
            signing_input.id for signing_input in signing_input_artifacts
        ]
        wr = self.playground.create_signing_task(
            task_name="sign",
            task_data=SignData(
                purpose=KeyPurpose.UEFI,
                unsigned=LookupMultiple.model_validate(signing_input_ids),
                key=asset_model.fingerprint,
            ),
            mark_running=True,
            **kwargs,
        )
        wr.dynamic_task_data = SignDynamicData(
            unsigned_ids=signing_input_ids
        ).model_dump()
        wr.save()
        return wr

    def test_permission_check_on_signing_input(self) -> None:
        workspace = self.playground.get_default_workspace()
        asset = self.playground.create_signing_key_asset()
        signing_input = self.playground.create_signing_input_artifact()
        work_request = self.create_sign_work_request(asset, [signing_input])
        token = self.playground.create_work_request_token(work_request)

        self.playground.create_artifact_relation(
            signing_input,
            target=self.playground.create_minimal_binary_package_artifact(),
        )

        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug=asset.slug,
            permission_name="sign_with",
            token=token,
            data={
                "artifact_id": signing_input.id,
                "work_request_id": work_request.id,
                "workspace": workspace.name,
            },
        )
        body = self.assertAPIResponseOk(response)
        self.assertFalse(body["has_permission"])
        self.assertEqual(body["username"], work_request.created_by.username)
        self.assertEqual(body["user_id"], work_request.created_by.id)
        self.assertEqual(
            body["resource"],
            {
                "architecture": "all",
                "package": "hello",
                "source": "hello",
                "version": "1.0-1",
            },
        )

    def create_sign_repository_index_work_request(
        self, asset: Asset, suite: Collection, index: Artifact, **kwargs: Any
    ) -> WorkRequest:
        """Create a SignRepositoryIndex work request."""
        asset_model = asset.data_model
        assert isinstance(asset_model, SigningKeyData)
        wr = self.playground.create_signing_task(
            task_name="signrepositoryindex",
            task_data=SignRepositoryIndexData(
                suite_collection=f"{suite.name}@{CollectionCategory.SUITE}",
                unsigned=index.id,
                mode=SigningMode.CLEAR,
                signed_name="InRelease",
            ),
            mark_running=True,
            **kwargs,
        )
        wr.dynamic_task_data = SignRepositoryIndexDynamicData(
            signing_keys=[asset_model.fingerprint], unsigned_id=index.id
        ).model_dump()
        wr.save()
        return wr

    def test_permission_check_on_repository_index(self) -> None:
        workspace = self.playground.get_default_workspace()
        asset = self.playground.create_signing_key_asset()
        AssetUsage.objects.create(asset=asset, workspace=workspace)
        suite = self.playground.create_collection(
            "suite", CollectionCategory.SUITE
        )
        index = self.playground.create_repository_index(
            "Release", skip_add_files_in_store=True
        )
        work_request = self.create_sign_repository_index_work_request(
            asset, suite, index
        )
        token = self.playground.create_work_request_token(work_request)

        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug=asset.slug,
            permission_name="sign_repository_with",
            token=token,
            data={
                "artifact_id": index.id,
                "work_request_id": work_request.id,
                "workspace": workspace.name,
            },
        )
        body = self.assertAPIResponseOk(response)
        self.assertFalse(body["has_permission"])
        self.assertEqual(body["username"], work_request.created_by.username)
        self.assertEqual(body["user_id"], work_request.created_by.id)
        self.assertEqual(body["resource"], {"path": "Release"})

    def test_permission_check_worker_token(self) -> None:
        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug="openpgp:ABC123",
            permission_name="sign_with",
            token=self.worker.token,
            data={
                "artifact_id": 1,
                "work_request_id": 1,
                "workspace": "workspace",
            },
        )
        self.assertResponseProblem(
            response,
            "Error",
            detail_pattern="You do not have permission to perform this action.",
            status_code=status.HTTP_403_FORBIDDEN,
        )

    def test_permission_check_unrelated_work_request(self) -> None:
        user = self.playground.get_default_user()
        workspace = self.playground.get_default_workspace()
        asset = self.playground.create_signing_key_asset()
        AssetUsage.objects.create(asset=asset, workspace=workspace)
        artifact = self.playground.create_signing_input_artifact()
        work_request_1 = self.playground.create_worker_task(
            mark_pending=True, worker=self.worker
        )
        work_request_2 = self.playground.create_worker_task(
            mark_running=True, worker=self.worker
        )
        token = self.playground.create_work_request_token(work_request_2)
        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug=asset.slug,
            permission_name="sign_with",
            token=token,
            data={
                "artifact_id": artifact.id,
                "work_request_id": work_request_1.id,
                "workspace": workspace.name,
            },
        )
        self.assertResponseProblem(
            response,
            f"{user} cannot display work request {work_request_1.id}",
            status_code=status.HTTP_403_FORBIDDEN,
        )

    def test_permission_check_wrong_worker_type(self) -> None:
        work_request = self.playground.create_worker_task(
            mark_running=True, assign_new_worker=True
        )
        token = self.playground.create_work_request_token(work_request)
        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug="openpgp:ABC123",
            permission_name="sign_with",
            token=token,
            data={
                "artifact_id": 1,
                "work_request_id": 1,
                "workspace": "workspace",
            },
        )
        self.assertResponseProblem(
            response,
            "Forbidden",
            detail_pattern=r"Endpoint is not accessible to external workers",
            status_code=status.HTTP_403_FORBIDDEN,
        )

    def test_permission_check_missing_asset(self) -> None:
        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug="openpgp:ABC123",
            permission_name="sign_with",
            data={
                "artifact_id": 1,
                "work_request_id": 1,
                "workspace": "workspace",
            },
        )
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
        body = response.json()
        self.assertEqual(body["title"], "Asset matching query does not exist.")

    def test_permission_check_malformed_slug(self) -> None:
        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug="foobar",
            permission_name="sign_with",
            data={
                "artifact_id": 1,
                "work_request_id": 1,
                "workspace": "workspace",
            },
        )
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
        body = response.json()
        self.assertEqual(
            body["title"],
            (
                "Malformed asset_slug: not enough values to unpack "
                "(expected 2, got 1)"
            ),
        )

    def test_permission_check_missing_artifact(self) -> None:
        asset = self.playground.create_signing_key_asset()
        work_request = self.create_sign_work_request(asset, [])
        token = self.playground.create_work_request_token(work_request)
        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug=asset.slug,
            permission_name="sign_with",
            token=token,
            data={
                "artifact_id": 0,
                "work_request_id": work_request.id,
                "workspace": work_request.workspace.name,
            },
        )
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        body = response.json()
        self.assertEqual(
            body["title"], "Artifact matching query does not exist."
        )

    def test_permission_check_missing_work_request(self) -> None:
        asset = self.playground.create_signing_key_asset()
        artifact = self.playground.create_signing_input_artifact()
        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug=asset.slug,
            permission_name="sign_with",
            data={
                "artifact_id": artifact.id,
                "work_request_id": 0,
                "workspace": artifact.workspace.name,
            },
        )
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        body = response.json()
        self.assertEqual(
            body["title"], "WorkRequest matching query does not exist."
        )

    def test_permission_check_work_request_visible(self) -> None:
        # This is an impossible scenario but it's good to have the check for
        # completeness.
        asset = self.playground.create_signing_key_asset()
        artifact = self.playground.create_signing_input_artifact()
        another_scope = self.playground.get_or_create_scope(name="another")
        private_workspace = self.playground.create_workspace(
            scope=another_scope, public=False
        )
        user = self.playground.get_default_user()
        work_request = self.create_sign_work_request(
            asset, [artifact], workspace=private_workspace, created_by=user
        )
        token = self.playground.create_work_request_token(work_request)
        self.assertFalse(
            work_request.can_display(PermissionContext.for_ui_user(user))
        )
        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug=asset.slug,
            permission_name="sign_with",
            token=token,
            data={
                "artifact_id": artifact.id,
                "work_request_id": work_request.id,
                "workspace": artifact.workspace.name,
            },
        )
        self.assertResponseProblem(
            response,
            "Workspace not found",
            detail_pattern="Workspace System not found in scope debusine",
            status_code=status.HTTP_404_NOT_FOUND,
        )

    def test_permission_check_artifact_visible(self) -> None:
        asset = self.playground.create_signing_key_asset()
        another_scope = self.playground.get_or_create_scope(name="another")
        # Make the workspace initially public to be able to populate the work
        # request
        private_workspace = self.playground.create_workspace(
            scope=another_scope, public=True
        )
        artifact = self.playground.create_signing_input_artifact(
            workspace=private_workspace
        )
        user = self.playground.get_default_user()
        work_request = self.create_sign_work_request(
            asset, [artifact], created_by=user
        )
        token = self.playground.create_work_request_token(work_request)
        private_workspace.public = False
        private_workspace.save()

        self.assertFalse(
            artifact.can_display(PermissionContext.for_ui_user(user))
        )
        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug=asset.slug,
            permission_name="sign_with",
            token=token,
            data={
                "artifact_id": artifact.id,
                "work_request_id": work_request.id,
                "workspace": artifact.workspace.name,
            },
        )
        self.assertResponseProblem(
            response,
            title=f"{user} cannot display artifact {artifact.id}",
            status_code=status.HTTP_403_FORBIDDEN,
        )

    def test_permission_check_artifact_unrelated(self) -> None:
        asset = self.playground.create_signing_key_asset()
        artifact = self.playground.create_signing_input_artifact()
        user = self.playground.get_default_user()
        work_request = self.create_sign_work_request(asset, [], created_by=user)
        token = self.playground.create_work_request_token(work_request)
        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug=asset.slug,
            permission_name="sign_with",
            token=token,
            data={
                "artifact_id": artifact.id,
                "work_request_id": work_request.id,
                "workspace": artifact.workspace.name,
            },
        )
        self.assertResponseProblem(
            response,
            f"{user} cannot display artifact {artifact.id}",
            status_code=status.HTTP_403_FORBIDDEN,
        )

    def test_permission_check_wrong_workspace(self) -> None:
        asset = self.playground.create_signing_key_asset()
        artifact = self.playground.create_signing_input_artifact()
        work_request = self.create_sign_work_request(asset, [artifact])
        token = self.playground.create_work_request_token(work_request)
        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug=asset.slug,
            permission_name="sign_with",
            token=token,
            data={
                "artifact_id": artifact.id,
                "work_request_id": work_request.id,
                "workspace": "another-workspace",
            },
        )
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        body = response.json()
        self.assertEqual(body["title"], "workspace does not match work request")

    def test_permission_check_unknown_permission(self) -> None:
        asset = self.playground.create_signing_key_asset()
        artifact = self.playground.create_signing_input_artifact()
        work_request = self.create_sign_work_request(asset, [artifact])
        token = self.playground.create_work_request_token(work_request)
        response = self.post_permission_check(
            asset_category=AssetCategory.SIGNING_KEY,
            asset_slug=asset.slug,
            permission_name="unknown",
            token=token,
            data={
                "artifact_id": artifact.id,
                "work_request_id": work_request.id,
                "workspace": artifact.workspace.name,
            },
        )
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        body = response.json()
        self.assertEqual(body["title"], "Unknown permission 'unknown'")

    def test_describe_artifact_repository_index(self) -> None:
        artifact = self.playground.create_repository_index("Release")
        result = AssetPermissionCheckView.describe_artifact(artifact)
        self.assertEqual(result, {"path": "Release"})

    def test_describe_artifact_signing_input_standalone(self) -> None:
        artifact = self.playground.create_signing_input_artifact()
        with self.assertLogsContains(
            (
                f"INFO:debusine.server.views.assets:Unable to fully describe "
                f"artifact {artifact!r}, no related debian:binary-package."
            ),
            logger=log,
        ):
            result = AssetPermissionCheckView.describe_artifact(artifact)
            self.assertEqual(result, {"package": "hello"})

    def test_describe_artifact_signing_input_multiple_relations(self) -> None:
        artifact = self.playground.create_signing_input_artifact()
        for _ in range(2):
            self.playground.create_artifact_relation(
                artifact,
                target=self.playground.create_minimal_binary_package_artifact(),
            )
        with self.assertLogsContains(
            (
                f"INFO:debusine.server.views.assets:Unable to fully describe "
                f"artifact {artifact!r}, multiple related "
                f"debian:binary-package."
            ),
            logger=log,
        ):
            result = AssetPermissionCheckView.describe_artifact(artifact)
            self.assertEqual(result, {"package": "hello"})

    def test_describe_artifact_signing_input_without_name(self) -> None:
        """Looks up the package from the debian:binary-package, if missing."""
        artifact = self.playground.create_signing_input_artifact(
            binary_package_name=None
        )
        self.playground.create_artifact_relation(
            artifact,
            target=self.playground.create_minimal_binary_package_artifact(),
        )
        result = AssetPermissionCheckView.describe_artifact(artifact)
        self.assertEqual(result["package"], "hello")

    def test_describe_artifact_upload_unknown(self) -> None:
        upload_artifacts = self.playground.create_upload_artifacts(binary=False)
        artifact = upload_artifacts.upload
        artifact.data["type"] = "unknown"
        with self.assertRaisesRegex(
            NotImplementedError,
            f"Unable to describe upload artifact {artifact}",
        ):
            AssetPermissionCheckView.describe_artifact(artifact)

    def test_describe_artifact_unknown(self) -> None:
        artifact = self.playground.create_minimal_binary_package_artifact()
        with self.assertRaisesRegex(
            NotImplementedError,
            f"Unable to describe artifact {artifact}",
        ):
            AssetPermissionCheckView.describe_artifact(artifact)
