# 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.

"""Test views for package archives."""

import base64
import datetime as dt
import hashlib
from abc import ABC
from collections.abc import Mapping
from contextlib import ExitStack
from operator import attrgetter
from pathlib import PurePath
from typing import Any, ClassVar, overload, override
from unittest import mock

from debian.copyright import parse_multiline
from debian.deb822 import Deb822
from django.conf import settings
from django.test import override_settings
from django.utils import timezone
from django.utils.cache import has_vary_header
from pgpy import PGPKey, PGPKeyring
from pgpy.constants import EllipticCurveOID, PubKeyAlgorithm
from rest_framework import HTTP_HEADER_ENCODING, status

from debusine.artifacts.models import CollectionCategory
from debusine.assets.models import KeyPurpose
from debusine.db.models import (
    Collection,
    CollectionItem,
    CollectionRelation,
    Token,
    Workspace,
)
from debusine.db.playground import scenarios
from debusine.test.django import TestCase, TestResponseType
from debusine.web.archives.views import RenderableSuite, SigningKeysResponse


class SigningKeysResponseTests(TestCase):
    """Tests for :py:class:`SigningKeysResponse`."""

    def test_render(self) -> None:
        keys = [
            PGPKey.new(PubKeyAlgorithm.EdDSA, EllipticCurveOID.Ed25519).pubkey
            for _ in range(2)
        ]
        response = SigningKeysResponse([bytes(key) for key in keys])
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(
            response.headers["Content-Type"],
            "application/pgp-keys; charset=utf-8",
        )
        assert response.content.startswith(
            b"-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
        )
        self.assertEqual(
            PGPKeyring(response.content).fingerprints(),
            {key.fingerprint for key in keys},
        )


class ArchiveViewTests(TestCase, ABC):
    """Helper methods for testing archive views."""

    playground_memory_file_store = False
    scenario = scenarios.DefaultContext()

    archive: ClassVar[Collection]
    suites: ClassVar[dict[str, Collection]]

    # Path used for some tests of generic behaviour in each subclass.
    example_path: str
    # Are paths tested here normally mutable?
    expect_mutable_no_archive: bool
    expect_mutable: bool

    @override
    @classmethod
    def setUpTestData(cls) -> None:
        super().setUpTestData()
        cls.archive = cls.scenario.workspace.get_singleton_collection(
            pc=cls.scenario.context_ui(), category=CollectionCategory.ARCHIVE
        )
        cls.suites = {
            name: cls.playground.create_collection(
                name, CollectionCategory.SUITE, workspace=cls.scenario.workspace
            )
            for name in ("bookworm", "trixie")
        }

    @overload
    def format_snapshot(self, snapshot: None) -> None: ...

    @overload
    def format_snapshot(self, snapshot: dt.datetime) -> str: ...

    def format_snapshot(self, snapshot: dt.datetime | None) -> str | None:
        """Format a snapshot timestamp as a URL segment."""
        return None if snapshot is None else snapshot.strftime("%Y%m%dT%H%M%SZ")

    def archive_file_get(
        self,
        *,
        workspace_name: str | None = None,
        snapshot: dt.datetime | None = None,
        path: str,
        headers: Mapping[str, Any] | None = None,
        token: Token | None = None,
    ) -> TestResponseType:
        """Get a file from an archive."""
        with ExitStack() as stack:
            fqdn = settings.DEBUSINE_DEBIAN_ARCHIVE_PRIMARY_FQDN
            scope_name = self.scenario.scope.name
            if workspace_name is None:
                workspace_name = self.scenario.workspace.name
            url = f"/{scope_name}/{workspace_name}"
            if snapshot is not None:
                url += "/" + self.format_snapshot(snapshot)
            url += f"/{path}"
            if token is not None:
                headers = {**(headers or {})}
                assert token.user is not None
                credentials = f"{token.user.username}:{token.key}"
                base64_credentials = base64.b64encode(
                    credentials.encode(HTTP_HEADER_ENCODING)
                ).decode("ascii")
                headers["Authorization"] = "Basic " + base64_credentials
                stack.enter_context(
                    mock.patch(
                        "debusine.server.middlewares.scopes."
                        "AuthorizationMiddleware._basic_auth_allowed",
                        return_value=True,
                    )
                )
            return self.client.get(url, HTTP_HOST=fqdn, headers=headers)

    def assert_has_caching_headers(
        self, response: TestResponseType, *, mutable: bool
    ) -> None:
        self.assertEqual(
            response.headers["Cache-Control"],
            "max-age=1800, proxy-revalidate" if mutable else "max-age=31536000",
        )
        self.assertTrue(has_vary_header(response, "Authorization"))

    def assertResponse404(
        self, response: TestResponseType, message: str
    ) -> None:
        """Check that the response is a 404 with the given message."""
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
        self.assertEqual(response.context["exception"], message)

    def test_workspace_not_found(self) -> None:
        for snapshot in (None, timezone.now()):
            with self.subTest(snapshot=self.format_snapshot(snapshot)):
                response = self.archive_file_get(
                    workspace_name="nonexistent",
                    snapshot=snapshot,
                    path=self.example_path,
                )

                self.assertResponse404(
                    response,
                    "Workspace nonexistent not found in scope debusine,"
                    " or you are not authorized to see it",
                )
                self.assert_has_caching_headers(
                    response,
                    mutable=self.expect_mutable_no_archive and snapshot is None,
                )

    def test_archive_not_found(self) -> None:
        self.archive.child_items.all().delete()
        self.archive.delete()

        for snapshot in (None, timezone.now()):
            with self.subTest(snapshot=self.format_snapshot(snapshot)):
                response = self.archive_file_get(
                    snapshot=snapshot, path=self.example_path
                )

                self.assertResponse404(
                    response,
                    f"No {CollectionCategory.ARCHIVE} collection found in "
                    f"{self.scenario.workspace}",
                )
                self.assert_has_caching_headers(
                    response,
                    mutable=self.expect_mutable_no_archive and snapshot is None,
                )


class ArchiveFileViewTests(ArchiveViewTests, ABC):
    """Helper methods for testing archive file views."""

    def assert_response_ok(
        self, response: TestResponseType, *, contents: bytes, mutable: bool
    ) -> None:
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(
            b"".join(getattr(response, "streaming_content")), contents
        )
        self.assert_has_caching_headers(response, mutable=mutable)

    def test_file_not_found(self) -> None:
        for snapshot in (None, timezone.now()):
            with self.subTest(snapshot=self.format_snapshot(snapshot)):
                response = self.archive_file_get(
                    snapshot=snapshot, path=self.example_path
                )

                self.assertResponse404(
                    response, "No FileInArtifact matches the given query."
                )
                self.assert_has_caching_headers(
                    response, mutable=self.expect_mutable and snapshot is None
                )


class ArchiveRootViewTests(ArchiveViewTests):
    """Test retrieving the root of an archive."""

    example_path = ""
    expect_mutable_no_archive = True
    expect_mutable = True

    def test_current(self) -> None:
        self.playground.create_collection(
            "not-exported",
            CollectionCategory.SUITE,
            workspace=self.scenario.workspace,
            data={"exported": False},
        )

        response = self.archive_file_get(path="")

        tree = self.assertResponseHTML(response)
        archive_suites = tree.xpath("//div[@id='archive-suites']")[0]
        self.assertTextContentEqual(
            archive_suites.p, "This archive contains the following suites:"
        )
        self.assertEqual(len(archive_suites.ul.li), len(self.suites))
        fqdn = settings.DEBUSINE_DEBIAN_ARCHIVE_FQDN
        workspace = self.scenario.workspace
        for li, suite in zip(
            archive_suites.ul.li,
            sorted(self.suites.values(), key=attrgetter("name")),
        ):
            self.assertTextContentEqual(li.p, f"{suite.name}: ")
            self.assertEqual(
                Deb822("".join(li.pre.itertext())),
                {
                    "Types": "deb",
                    "URIs": (
                        f"http://{fqdn}/{workspace.scope.name}/{workspace.name}"
                    ),
                    "Suites": suite.name,
                },
            )
        self.assert_has_caching_headers(response, mutable=True)

    def test_current_signing_keys(self) -> None:
        keys = [
            PGPKey.new(PubKeyAlgorithm.EdDSA, EllipticCurveOID.Ed25519).pubkey
            for _ in range(len(self.suites))
        ]
        sorted_suites = sorted(self.suites.values(), key=attrgetter("name"))
        for suite, key in zip(sorted_suites, keys):
            self.playground.create_asset_usage(
                resource=self.playground.create_signing_key_asset(
                    purpose=KeyPurpose.OPENPGP_REPOSITORY,
                    fingerprint=key.fingerprint,
                    public_key=str(key),
                    workspace=suite.workspace,
                ),
                workspace=suite.workspace,
            )
            suite.data["signing_keys"] = [key.fingerprint]
            suite.save()

        response = self.archive_file_get(path="")

        tree = self.assertResponseHTML(response)
        archive_suites = tree.xpath("//div[@id='archive-suites']")[0]
        self.assertTextContentEqual(
            archive_suites.p, "This archive contains the following suites:"
        )
        self.assertEqual(len(archive_suites.ul.li), len(self.suites))
        fqdn = settings.DEBUSINE_DEBIAN_ARCHIVE_FQDN
        workspace = self.scenario.workspace
        for li, suite, key in zip(archive_suites.ul.li, sorted_suites, keys):
            self.assertTextContentEqual(li.p, f"{suite.name}: ")
            suite_deb822 = Deb822("".join(li.pre.itertext()))
            self.assertCountEqual(
                suite_deb822.keys(), ["Types", "URIs", "Suites", "Signed-By"]
            )
            self.assertDictContainsAll(
                suite_deb822,
                {
                    "Types": "deb",
                    "URIs": (
                        f"http://{fqdn}/{workspace.scope.name}/{workspace.name}"
                    ),
                    "Suites": suite.name,
                },
            )
            self.assertEqual(
                PGPKeyring(
                    parse_multiline(suite_deb822["Signed-By"])
                ).fingerprints(),
                {key.fingerprint},
            )
        self.assert_has_caching_headers(response, mutable=True)

    def test_snapshot(self) -> None:
        now = timezone.now()
        one_month_ago = (now - dt.timedelta(days=30)).replace(microsecond=0)

        response = self.archive_file_get(snapshot=one_month_ago, path="")

        # We always list all exported suites in the workspace, regardless of
        # when they were added to the archive, so we don't bother testing
        # different snapshot times here.
        tree = self.assertResponseHTML(response)
        archive_suites = tree.xpath("//div[@id='archive-suites']")[0]
        self.assertTextContentEqual(
            archive_suites.p, "This archive contains the following suites:"
        )
        self.assertEqual(len(archive_suites.ul.li), len(self.suites))
        fqdn = settings.DEBUSINE_DEBIAN_ARCHIVE_FQDN
        workspace = self.scenario.workspace
        for li, suite in zip(
            archive_suites.ul.li,
            sorted(self.suites.values(), key=attrgetter("name")),
        ):
            self.assertTextContentEqual(li.p, f"{suite.name}: ")
            self.assertEqual(
                Deb822("".join(li.pre.itertext())),
                {
                    "Types": "deb",
                    "URIs": (
                        f"http://{fqdn}/{workspace.scope.name}/{workspace.name}"
                    ),
                    "Suites": suite.name,
                    "Snapshot": self.format_snapshot(one_month_ago),
                },
            )
        self.assert_has_caching_headers(response, mutable=False)

    def test_archive_empty(self) -> None:
        self.archive.child_items.all().delete()
        for suite in self.suites.values():
            suite.delete()

        for snapshot in (None, timezone.now()):
            with self.subTest(snapshot=self.format_snapshot(snapshot)):
                response = self.archive_file_get(snapshot=snapshot, path="")

                tree = self.assertResponseHTML(response)
                archive_suites = tree.xpath("//div[@id='archive-suites']")[0]
                self.assertTextContentEqual(
                    archive_suites, "This archive is empty."
                )
                self.assert_has_caching_headers(
                    response, mutable=snapshot is None
                )

    @override_settings(
        ALLOWED_HOSTS=["*"],
        DEBUSINE_DEBIAN_ARCHIVE_FQDN=["deb.example.com", "deb.example.org"],
        DEBUSINE_DEBIAN_ARCHIVE_PRIMARY_FQDN="deb.example.com",
    )
    def test_override_archive_fqdn(self) -> None:
        response = self.archive_file_get(path="")

        tree = self.assertResponseHTML(response)
        archive_suites = tree.xpath("//div[@id='archive-suites']")[0]
        workspace = self.scenario.workspace
        for li in archive_suites.ul.li:
            self.assertEqual(
                Deb822("".join(li.pre.itertext()))["URIs"],
                f"http://deb.example.com/"
                f"{workspace.scope.name}/{workspace.name}",
            )

    def test_private_workspace_not_accessible(self) -> None:
        self.scenario.workspace.public = False
        self.scenario.workspace.save()
        response = self.archive_file_get(path="")
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

    def test_private_workspace_logged_in(self) -> None:
        self.scenario.workspace.public = False
        self.scenario.workspace.save()
        token = self.scenario.create_user_token()
        with self.scenario.assign_role(
            self.scenario.workspace, Workspace.Roles.CONTRIBUTOR
        ):
            response = self.archive_file_get(path="", token=token)
        self.assertResponseHTML(response)


class SuiteRootViewTests(ArchiveViewTests):
    """Test retrieving the root of a suite."""

    example_path = "dists/bookworm"
    expect_mutable_no_archive = True
    expect_mutable = True

    def test_current(self) -> None:
        suite = self.suites["bookworm"]
        suite.data["components"] = ["main", "contrib", "non-free"]
        suite.data["architectures"] = ["amd64"]
        suite.data["release_fields"] = {"Description": "Something special"}
        suite.save()

        response = self.archive_file_get(path="dists/bookworm/")

        tree = self.assertResponseHTML(response)
        self.assertTextContentEqual(
            tree.xpath("//div[@id='suite-summary']")[0],
            "bookworm: Something special",
        )
        suite_architectures = tree.xpath("//div[@id='suite-architectures']")[0]
        self.assertEqual(
            [li.text for li in suite_architectures.ul.li], ["amd64"]
        )
        suite_release_fields = tree.xpath("//div[@id='suite-release-fields']")[
            0
        ]
        self.assertEqual(
            [
                (element.tag, element.text.strip())
                for element in suite_release_fields.dl.iterchildren()
            ],
            [("dt", "Description"), ("dd", "Something special")],
        )
        suite_apt = tree.xpath("//div[@id='suite-apt']")[0]
        fqdn = settings.DEBUSINE_DEBIAN_ARCHIVE_FQDN
        workspace = self.scenario.workspace
        self.assertEqual(
            Deb822("".join(suite_apt.pre.itertext())),
            {
                "Types": "deb",
                "URIs": (
                    f"http://{fqdn}/{workspace.scope.name}/{workspace.name}"
                ),
                "Suites": "bookworm",
                "Components": "main contrib non-free",
            },
        )
        self.assert_has_caching_headers(response, mutable=True)

    def test_current_with_required_suites(self) -> None:
        personal = self.playground.create_collection(
            "r-personal",
            CollectionCategory.SUITE,
            workspace=self.scenario.workspace,
        )
        CollectionRelation.objects.create(
            source=personal,
            target=self.suites["trixie"],
            created_by_user=self.playground.get_default_user(),
            type=CollectionRelation.Types.SUITE_REQUIRES,
            position=1,
        )

        response = self.archive_file_get(path="dists/r-personal/")

        tree = self.assertResponseHTML(response)

        fqdn = settings.DEBUSINE_DEBIAN_ARCHIVE_FQDN
        workspace = self.scenario.workspace
        uri = f"http://{fqdn}/{workspace.scope.name}/{workspace.name}"

        required_apt_sources = "".join(
            tree.xpath("//div[@id='suite-required-apt']/pre/code/text()")
        )

        self.assertEqual(
            list(Deb822.iter_paragraphs(required_apt_sources)),
            [
                {
                    "Types": "deb",
                    "URIs": uri,
                    "Suites": "trixie",
                },
            ],
        )

    def test_current_signing_keys(self) -> None:
        suite = self.suites["bookworm"]
        keys = [
            PGPKey.new(PubKeyAlgorithm.EdDSA, EllipticCurveOID.Ed25519).pubkey
            for _ in range(2)
        ]
        for key in keys:
            self.playground.create_asset_usage(
                resource=self.playground.create_signing_key_asset(
                    purpose=KeyPurpose.OPENPGP_REPOSITORY,
                    fingerprint=key.fingerprint,
                    public_key=str(key),
                    workspace=suite.workspace,
                ),
                workspace=suite.workspace,
            )
        suite.data["signing_keys"] = [key.fingerprint for key in keys]
        suite.save()

        response = self.archive_file_get(path="dists/bookworm/")

        tree = self.assertResponseHTML(response)
        self.assertTextContentEqual(
            tree.xpath("//div[@id='suite-summary']")[0], "bookworm:"
        )
        self.assertFalse(tree.xpath("//div[@id='suite-architectures']"))
        self.assertFalse(tree.xpath("//div[@id='suite-release-fields']"))
        fqdn = settings.DEBUSINE_DEBIAN_ARCHIVE_FQDN
        workspace = self.scenario.workspace
        suite_apt = tree.xpath("//div[@id='suite-apt']")[0]
        suite_deb822 = Deb822("".join(suite_apt.pre.itertext()))
        self.assertCountEqual(
            suite_deb822.keys(), ["Types", "URIs", "Suites", "Signed-By"]
        )
        self.assertDictContainsAll(
            suite_deb822,
            {
                "Types": "deb",
                "URIs": (
                    f"http://{fqdn}/{workspace.scope.name}/{workspace.name}"
                ),
                "Suites": "bookworm",
            },
        )
        self.assertEqual(
            PGPKeyring(
                parse_multiline(suite_deb822["Signed-By"])
            ).fingerprints(),
            {key.fingerprint for key in keys},
        )
        self.assert_has_caching_headers(response, mutable=True)

    def test_snapshot(self) -> None:
        now = timezone.now()
        one_month_ago = (now - dt.timedelta(days=30)).replace(microsecond=0)
        self.archive.child_items.filter(
            category=CollectionCategory.SUITE, name="bookworm"
        ).update(created_at=one_month_ago)

        # We always show an exported suite, regardless of when it was added
        # to the archive, so we don't bother testing different snapshot
        # times here.
        response = self.archive_file_get(
            snapshot=one_month_ago, path="dists/bookworm/"
        )

        tree = self.assertResponseHTML(response)
        self.assertTextContentEqual(
            tree.xpath("//div[@id='suite-summary']")[0], "bookworm:"
        )
        self.assertFalse(tree.xpath("//div[@id='suite-architectures']"))
        self.assertFalse(tree.xpath("//div[@id='suite-release-fields']"))
        fqdn = settings.DEBUSINE_DEBIAN_ARCHIVE_FQDN
        suite_apt = tree.xpath("//div[@id='suite-apt']")[0]
        workspace = self.scenario.workspace
        self.assertEqual(
            Deb822("".join(suite_apt.pre.itertext())),
            {
                "Types": "deb",
                "URIs": (
                    f"http://{fqdn}/{workspace.scope.name}/{workspace.name}"
                ),
                "Suites": "bookworm",
                "Snapshot": self.format_snapshot(one_month_ago),
            },
        )
        self.assert_has_caching_headers(response, mutable=False)

    def test_suite_not_found(self) -> None:
        for snapshot in (None, timezone.now()):
            with self.subTest(snapshot=self.format_snapshot(snapshot)):
                response = self.archive_file_get(
                    snapshot=snapshot, path="dists/nonexistent/"
                )

                self.assertResponse404(response, "Suite not found")
                self.assert_has_caching_headers(
                    response, mutable=snapshot is None
                )

    def test_suite_not_exported(self) -> None:
        self.playground.create_collection(
            "not-exported",
            CollectionCategory.SUITE,
            workspace=self.scenario.workspace,
            data={"exported": False},
        )

        for snapshot in (None, timezone.now()):
            with self.subTest(snapshot=self.format_snapshot(snapshot)):
                response = self.archive_file_get(
                    snapshot=snapshot, path="dists/not-exported/"
                )

                self.assertResponse404(response, "Suite not found")
                self.assert_has_caching_headers(
                    response, mutable=snapshot is None
                )


class SigningKeysViewTests(ArchiveViewTests):
    """Test retrieving signing keys for an archive or a suite."""

    example_path = "signing-keys.asc"
    expect_mutable_no_archive = True
    expect_mutable = True

    def test_archive(self) -> None:
        keys = [
            PGPKey.new(PubKeyAlgorithm.EdDSA, EllipticCurveOID.Ed25519).pubkey
            for _ in range(len(self.suites))
        ]
        for suite, key in zip(self.suites.values(), keys):
            self.playground.create_asset_usage(
                resource=self.playground.create_signing_key_asset(
                    purpose=KeyPurpose.OPENPGP_REPOSITORY,
                    fingerprint=key.fingerprint,
                    public_key=str(key),
                    workspace=suite.workspace,
                ),
                workspace=suite.workspace,
            )
            suite.data["signing_keys"] = [key.fingerprint]
            suite.save()

        response = self.archive_file_get(path="signing-keys.asc")

        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(
            response.headers["Content-Type"],
            "application/pgp-keys; charset=utf-8",
        )
        self.assertEqual(
            PGPKeyring(response.content).fingerprints(),
            {key.fingerprint for key in keys},
        )
        self.assert_has_caching_headers(response, mutable=True)

    def test_suite(self) -> None:
        suite = self.suites["bookworm"]
        keys = [
            PGPKey.new(PubKeyAlgorithm.EdDSA, EllipticCurveOID.Ed25519).pubkey
            for _ in range(len(self.suites))
        ]
        for key in keys:
            self.playground.create_asset_usage(
                resource=self.playground.create_signing_key_asset(
                    purpose=KeyPurpose.OPENPGP_REPOSITORY,
                    fingerprint=key.fingerprint,
                    public_key=str(key),
                    workspace=suite.workspace,
                ),
                workspace=suite.workspace,
            )
        suite.data["signing_keys"] = [key.fingerprint for key in keys]
        suite.save()

        response = self.archive_file_get(path="dists/bookworm/signing-keys.asc")

        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(
            response.headers["Content-Type"],
            "application/pgp-keys; charset=utf-8",
        )
        self.assertEqual(
            PGPKeyring(response.content).fingerprints(),
            {key.fingerprint for key in keys},
        )
        self.assert_has_caching_headers(response, mutable=True)


class DistsByHashFileViewTests(ArchiveFileViewTests):
    """Test retrieving ``by-hash`` files from ``dists/``."""

    example_path = "dists/bookworm/main/source/by-hash/SHA256/00"
    expect_mutable_no_archive = False
    expect_mutable = False

    def test_current(self) -> None:
        old_contents = b"Old Sources file\n"
        old_sha256 = hashlib.sha256(old_contents).hexdigest()
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index(
                "Sources.xz", contents=old_contents
            ),
            user=self.scenario.user,
            variables={"path": "main/source/Sources.xz"},
        )
        new_contents = b"New Sources file\n"
        new_sha256 = hashlib.sha256(new_contents).hexdigest()
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index(
                "Sources.xz", contents=new_contents
            ),
            user=self.scenario.user,
            variables={"path": "main/source/Sources.xz"},
            replace=True,
        )
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index("Packages.xz"),
            user=self.scenario.user,
            variables={"path": "main/binary-amd64/Packages.xz"},
        )
        self.suites["trixie"].manager.add_artifact(
            self.playground.create_repository_index("Sources.xz"),
            user=self.scenario.user,
            variables={"path": "main/source/Sources.xz"},
        )

        response = self.archive_file_get(
            path=f"dists/bookworm/main/source/by-hash/SHA256/{old_sha256}"
        )

        self.assert_response_ok(response, contents=old_contents, mutable=False)

        response = self.archive_file_get(
            path=f"dists/bookworm/main/source/by-hash/SHA256/{new_sha256}"
        )

        self.assert_response_ok(response, contents=new_contents, mutable=False)

    def test_snapshot(self) -> None:
        one_month_ago = (timezone.now() - dt.timedelta(days=30)).replace(
            microsecond=0
        )
        self.archive.child_items.filter(
            category=CollectionCategory.SUITE
        ).update(created_at=one_month_ago)
        old_contents = b"Old Sources file\n"
        old_sha256 = hashlib.sha256(old_contents).hexdigest()
        old_item = self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index(
                "Sources.xz", contents=old_contents
            ),
            user=self.scenario.user,
            variables={"path": "main/source/Sources.xz"},
        )
        old_item.created_at = one_month_ago
        old_item.save()
        new_contents = b"New Sources file\n"
        new_sha256 = hashlib.sha256(new_contents).hexdigest()
        new_item = self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index(
                "Sources.xz", contents=new_contents
            ),
            user=self.scenario.user,
            variables={"path": "main/source/Sources.xz"},
            replace=True,
        )
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index("Packages.xz"),
            user=self.scenario.user,
            variables={"path": "main/binary-amd64/Packages.xz"},
        )
        self.suites["trixie"].manager.add_artifact(
            self.playground.create_repository_index("Sources.xz"),
            user=self.scenario.user,
            variables={"path": "main/source/Sources.xz"},
        )

        for snapshot, sha256, expected_contents in (
            (old_item.created_at - dt.timedelta(seconds=1), old_sha256, None),
            (old_item.created_at - dt.timedelta(seconds=1), new_sha256, None),
            (old_item.created_at, old_sha256, old_contents),
            (old_item.created_at, new_sha256, None),
            (
                new_item.created_at - dt.timedelta(seconds=1),
                old_sha256,
                old_contents,
            ),
            (new_item.created_at - dt.timedelta(seconds=1), new_sha256, None),
            # Snapshot URLs don't include a microsecond component, so add a
            # second to avoid rounding problems.
            (
                new_item.created_at + dt.timedelta(seconds=1),
                old_sha256,
                old_contents,
            ),
            (
                new_item.created_at + dt.timedelta(seconds=1),
                new_sha256,
                new_contents,
            ),
        ):
            with self.subTest(
                snapshot=self.format_snapshot(snapshot), sha256=sha256
            ):
                response = self.archive_file_get(
                    snapshot=snapshot,
                    path=f"dists/bookworm/main/source/by-hash/SHA256/{sha256}",
                )

                if expected_contents is None:
                    self.assertResponse404(
                        response, "No FileInArtifact matches the given query."
                    )
                else:
                    self.assert_response_ok(
                        response, contents=expected_contents, mutable=False
                    )

    def test_suite_not_exported(self) -> None:
        contents = b"Sources file\n"
        sha256 = hashlib.sha256(contents).hexdigest()
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index(
                "Sources.xz", contents=contents
            ),
            user=self.scenario.user,
            variables={"path": "main/source/Sources.xz"},
            replace=True,
        )

        response = self.archive_file_get(
            path=f"dists/bookworm/main/source/by-hash/SHA256/{sha256}"
        )

        self.assert_response_ok(response, contents=contents, mutable=False)

        self.suites["bookworm"].data["exported"] = False
        self.suites["bookworm"].save()

        response = self.archive_file_get(
            path=f"dists/bookworm/main/source/by-hash/SHA256/{sha256}"
        )

        self.assertResponse404(
            response, "No FileInArtifact matches the given query."
        )
        self.assert_has_caching_headers(response, mutable=False)

    def test_ignores_content_negotiation(self) -> None:
        contents = b"Sources file"
        sha256 = hashlib.sha256(contents).hexdigest()
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index(
                "Sources.xz", contents=contents
            ),
            user=self.scenario.user,
            variables={"path": "main/source/Sources.xz"},
        )

        response = self.archive_file_get(
            path=f"dists/bookworm/main/source/by-hash/SHA256/{sha256}",
            headers={"Accept": "application/octet-stream"},
        )

        self.assert_response_ok(
            response, contents=b"Sources file", mutable=False
        )

    def test_by_hash_matches_multiple_artifacts(self) -> None:
        contents = b"Sources file"
        sha256 = hashlib.sha256(contents).hexdigest()
        for _ in range(2):
            self.suites["bookworm"].manager.add_artifact(
                self.playground.create_repository_index(
                    "Sources.xz", contents=contents
                ),
                replace=True,
                user=self.scenario.user,
                variables={"path": "main/source/Sources.xz"},
            )

        response = self.archive_file_get(
            path=f"dists/bookworm/main/source/by-hash/SHA256/{sha256}",
        )

        self.assert_response_ok(
            response, contents=b"Sources file", mutable=False
        )


class DistsFileViewTests(ArchiveFileViewTests):
    """Test retrieving non-``by-hash`` files from ``dists/``."""

    example_path = "dists/bookworm/Release"
    expect_mutable_no_archive = True
    expect_mutable = True

    def test_current(self) -> None:
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index(
                "Release", contents=b"Old Release file\n"
            ),
            user=self.scenario.user,
            variables={"path": "Release"},
        )
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index(
                "Release", contents=b"New Release file\n"
            ),
            user=self.scenario.user,
            variables={"path": "Release"},
            replace=True,
        )
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index("Sources.xz"),
            user=self.scenario.user,
            variables={"path": "main/source/Sources.xz"},
        )
        self.suites["trixie"].manager.add_artifact(
            self.playground.create_repository_index("Release"),
            user=self.scenario.user,
            variables={"path": "Release"},
        )

        response = self.archive_file_get(path="dists/bookworm/Release")

        self.assert_response_ok(
            response, contents=b"New Release file\n", mutable=True
        )

    def test_snapshot(self) -> None:
        one_month_ago = (timezone.now() - dt.timedelta(days=30)).replace(
            microsecond=0
        )
        self.archive.child_items.filter(
            category=CollectionCategory.SUITE
        ).update(created_at=one_month_ago)
        old_contents = b"Old Release file\n"
        old_item = self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index(
                "Release", contents=old_contents
            ),
            user=self.scenario.user,
            variables={"path": "Release"},
        )
        old_item.created_at = one_month_ago
        old_item.save()
        new_contents = b"New Release file\n"
        new_item = self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index(
                "Release", contents=new_contents
            ),
            user=self.scenario.user,
            variables={"path": "Release"},
            replace=True,
        )
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index("Sources.xz"),
            user=self.scenario.user,
            variables={"path": "main/source/Sources.xz"},
        )
        self.suites["trixie"].manager.add_artifact(
            self.playground.create_repository_index("Release"),
            user=self.scenario.user,
            variables={"path": "Release"},
        )

        for snapshot, expected_contents in (
            (old_item.created_at - dt.timedelta(seconds=1), None),
            (old_item.created_at, old_contents),
            (new_item.created_at - dt.timedelta(seconds=1), old_contents),
            # Snapshot URLs don't include a microsecond component, so add a
            # second to avoid rounding problems.
            (new_item.created_at + dt.timedelta(seconds=1), new_contents),
        ):
            with self.subTest(snapshot=self.format_snapshot(snapshot)):
                response = self.archive_file_get(
                    snapshot=snapshot, path="dists/bookworm/Release"
                )

                if expected_contents is None:
                    self.assertResponse404(
                        response, "No FileInArtifact matches the given query."
                    )
                else:
                    self.assert_response_ok(
                        response, contents=expected_contents, mutable=False
                    )

    def test_suite_not_exported(self) -> None:
        contents = b"Release file\n"
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index(
                "Release", contents=contents
            ),
            user=self.scenario.user,
            variables={"path": "Release"},
            replace=True,
        )

        response = self.archive_file_get(path="dists/bookworm/Release")

        self.assert_response_ok(response, contents=contents, mutable=True)

        self.suites["bookworm"].data["exported"] = False
        self.suites["bookworm"].save()

        response = self.archive_file_get(path="dists/bookworm/Release")

        self.assertResponse404(
            response, "No FileInArtifact matches the given query."
        )
        self.assert_has_caching_headers(response, mutable=True)

    def test_ignores_content_negotiation(self) -> None:
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_repository_index(
                "Release", contents=b"Release file"
            ),
            user=self.scenario.user,
            variables={"path": "Release"},
        )

        response = self.archive_file_get(
            path="dists/bookworm/Release",
            headers={"Accept": "application/octet-stream"},
        )

        self.assert_response_ok(
            response, contents=b"Release file", mutable=True
        )


class PoolFileViewTests(ArchiveFileViewTests):
    """Test retrieving files from ``pool/``."""

    example_path = "pool/main/h/hello/hello_1.0.dsc"
    expect_mutable_no_archive = True
    expect_mutable = False

    def test_current_source(self) -> None:
        for suite_name, component, name, version, paths in (
            (
                "bookworm",
                "main",
                "hello",
                "1.0",
                ["hello_1.0.dsc", "hello_1.0.tar.xz"],
            ),
            (
                "bookworm",
                "contrib",
                "hello",
                "1.1-1",
                [
                    "hello_1.1-1.dsc",
                    "hello_1.1-1.debian.tar.xz",
                    "hello_1.1.orig.tar.xz",
                ],
            ),
            (
                "bookworm",
                "contrib",
                "hello",
                "1.1-2",
                [
                    "hello_1.1-2.dsc",
                    "hello_1.1-2.debian.tar.xz",
                    "hello_1.1.orig.tar.xz",
                ],
            ),
            ("bookworm", "main", "base-files", "1.0", ["base-files_1.0.dsc"]),
            (
                "trixie",
                "main",
                "hello",
                "1.0",
                ["hello_1.0.dsc", "hello_1.0.tar.xz"],
            ),
        ):
            self.suites[suite_name].manager.add_artifact(
                self.playground.create_minimal_source_package_artifact(
                    name,
                    version,
                    paths={
                        path: f"Contents of {path}".encode() for path in paths
                    },
                    create_files=True,
                ),
                user=self.scenario.user,
                variables={"component": component, "section": "devel"},
            )

        for path in (
            "pool/main/h/hello/hello_1.0.dsc",
            "pool/main/h/hello/hello_1.0.tar.xz",
            "pool/contrib/h/hello/hello_1.1-1.dsc",
            "pool/contrib/h/hello/hello_1.1-1.debian.tar.xz",
            "pool/contrib/h/hello/hello_1.1-2.dsc",
            "pool/contrib/h/hello/hello_1.1-2.debian.tar.xz",
            "pool/contrib/h/hello/hello_1.1.orig.tar.xz",
        ):
            response = self.archive_file_get(path=path)

            self.assert_response_ok(
                response,
                contents=f"Contents of {PurePath(path).name}".encode(),
                mutable=False,
            )

    def test_current_binary(self) -> None:
        for (
            suite_name,
            component,
            srcpkg_name,
            srcpkg_version,
            name,
            version,
            architecture,
        ) in (
            ("bookworm", "main", "hello", "1.0", "libhello1", "1.0", "amd64"),
            ("bookworm", "main", "hello", "1.0", "libhello1", "1.0", "s390x"),
            ("bookworm", "main", "hello", "1.0", "libhello-doc", "1.0", "all"),
            (
                "bookworm",
                "contrib",
                "hello",
                "1.1",
                "libhello1",
                "1:1.1",
                "amd64",
            ),
            (
                "bookworm",
                "main",
                "base-files",
                "1.0",
                "base-files",
                "1.0",
                "amd64",
            ),
            ("trixie", "main", "hello", "1.0", "libhello1", "1.0", "amd64"),
        ):
            deb_path = f"{name}_{version.split(':')[-1]}_{architecture}.deb"
            self.suites[suite_name].manager.add_artifact(
                self.playground.create_minimal_binary_package_artifact(
                    srcpkg_name,
                    srcpkg_version,
                    name,
                    version,
                    architecture,
                    paths={deb_path: f"Contents of {deb_path}".encode()},
                    create_files=True,
                ),
                user=self.scenario.user,
                variables={
                    "component": component,
                    "section": "devel",
                    "priority": "optional",
                },
            )

        for path in (
            "pool/main/h/hello/libhello1_1.0_amd64.deb",
            "pool/main/h/hello/libhello1_1.0_s390x.deb",
            "pool/main/h/hello/libhello-doc_1.0_all.deb",
            "pool/contrib/h/hello/libhello1_1.1_amd64.deb",
        ):
            response = self.archive_file_get(path=path)

            self.assert_response_ok(
                response,
                contents=f"Contents of {PurePath(path).name}".encode(),
                mutable=False,
            )

    def test_snapshot_source(self) -> None:
        one_month_ago = (timezone.now() - dt.timedelta(days=30)).replace(
            microsecond=0
        )
        one_day_ago = (timezone.now() - dt.timedelta(days=1)).replace(
            microsecond=0
        )
        self.archive.child_items.filter(
            category=CollectionCategory.SUITE
        ).update(created_at=one_month_ago)
        items: list[CollectionItem] = []
        for version in ("1.1-1", "1.1-2", "1.1-3"):
            items.append(
                self.suites["bookworm"].manager.add_artifact(
                    self.playground.create_minimal_source_package_artifact(
                        "hello",
                        version,
                        paths={
                            path: f"Contents of {path}".encode()
                            for path in (
                                f"hello_{version}.dsc",
                                f"hello_{version.split('-')[0]}.orig.tar.xz",
                            )
                        },
                        create_files=True,
                    ),
                    user=self.scenario.user,
                    variables={"component": "main", "section": "devel"},
                )
            )
        items[0].created_at = one_month_ago
        items[0].removed_at = one_day_ago
        items[0].save()
        items[1].created_at = one_day_ago
        items[1].save()
        start = items[0].created_at - dt.timedelta(seconds=1)
        # Snapshot URLs don't include a microsecond component, so add a
        # second to avoid rounding problems.
        end = items[2].created_at + dt.timedelta(seconds=1)

        for snapshot, name, expected in (
            (start, "hello_1.1-1.dsc", False),
            (start, "hello_1.1-2.dsc", False),
            (start, "hello_1.1-3.dsc", False),
            (start, "hello_1.1.orig.tar.xz", False),
            (items[0].created_at, "hello_1.1-1.dsc", True),
            (items[0].created_at, "hello_1.1-2.dsc", False),
            (items[0].created_at, "hello_1.1-3.dsc", False),
            (items[0].created_at, "hello_1.1.orig.tar.xz", True),
            (items[1].created_at, "hello_1.1-1.dsc", False),
            (items[1].created_at, "hello_1.1-2.dsc", True),
            (items[1].created_at, "hello_1.1-3.dsc", False),
            (items[1].created_at, "hello_1.1.orig.tar.xz", True),
            (end, "hello_1.1-1.dsc", False),
            (end, "hello_1.1-2.dsc", True),
            (end, "hello_1.1-3.dsc", True),
            (end, "hello_1.1.orig.tar.xz", True),
        ):
            with self.subTest(
                snapshot=self.format_snapshot(snapshot), name=name
            ):
                response = self.archive_file_get(
                    snapshot=snapshot, path=f"pool/main/h/hello/{name}"
                )

                if expected:
                    self.assert_response_ok(
                        response,
                        contents=f"Contents of {name}".encode(),
                        mutable=False,
                    )
                else:
                    self.assertResponse404(
                        response, "No FileInArtifact matches the given query."
                    )

    def test_suite_not_exported(self) -> None:
        contents = b"dsc file\n"
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_minimal_source_package_artifact(
                "hello",
                "1.0",
                paths={"hello_1.0.dsc": contents},
                create_files=True,
            ),
            user=self.scenario.user,
            variables={"component": "main", "section": "devel"},
        )

        response = self.archive_file_get(path="pool/main/h/hello/hello_1.0.dsc")

        self.assert_response_ok(response, contents=contents, mutable=False)

        self.suites["bookworm"].data["exported"] = False
        self.suites["bookworm"].save()

        response = self.archive_file_get(path="pool/main/h/hello/hello_1.0.dsc")

        self.assertResponse404(
            response, "No FileInArtifact matches the given query."
        )
        self.assert_has_caching_headers(response, mutable=False)

    def test_wrong_component(self) -> None:
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_minimal_source_package_artifact(
                "hello", "1.0", paths=["hello_1.0.dsc"], create_files=True
            ),
            user=self.scenario.user,
            variables={"component": "main", "section": "devel"},
        )
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_minimal_binary_package_artifact(
                "hello",
                "1.0",
                "hello",
                "1.0",
                "amd64",
                paths=["hello_1.0_amd64.deb"],
                create_files=True,
            ),
            user=self.scenario.user,
            variables={
                "component": "main",
                "section": "devel",
                "priority": "optional",
            },
        )

        for name in ("hello_1.0.dsc", "hello_1.0_amd64.deb"):
            response = self.archive_file_get(
                path=f"pool/contrib/h/hello/{name}"
            )

            self.assertResponse404(
                response, "No FileInArtifact matches the given query."
            )

    def test_wrong_source_prefix(self) -> None:
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_minimal_source_package_artifact(
                "hello", "1.0", paths=["hello_1.0.dsc"], create_files=True
            ),
            user=self.scenario.user,
            variables={"component": "main", "section": "devel"},
        )
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_minimal_binary_package_artifact(
                "hello",
                "1.0",
                "hello",
                "1.0",
                "amd64",
                paths=["hello_1.0_amd64.deb"],
                create_files=True,
            ),
            user=self.scenario.user,
            variables={
                "component": "main",
                "section": "devel",
                "priority": "optional",
            },
        )

        for name in ("hello_1.0.dsc", "hello_1.0_amd64.deb"):
            response = self.archive_file_get(path=f"pool/main/a/hello/{name}")

            self.assertResponse404(
                response, "No FileInArtifact matches the given query."
            )

    def test_ignores_content_negotiation(self) -> None:
        self.suites["bookworm"].manager.add_artifact(
            self.playground.create_minimal_source_package_artifact(
                "hello",
                "1.0",
                paths={"hello_1.0.dsc": b"Contents of hello_1.0.dsc"},
                create_files=True,
            ),
            user=self.scenario.user,
            variables={"component": "main", "section": "devel"},
        )

        response = self.archive_file_get(
            path="pool/main/h/hello/hello_1.0.dsc",
            headers={"Accept": "application/octet-stream"},
        )

        self.assert_response_ok(
            response, contents=b"Contents of hello_1.0.dsc", mutable=False
        )


class TopLevelFileViewTests(ArchiveFileViewTests):
    """Test retrieving top-level files, not in any suite."""

    example_path = "README"
    expect_mutable_no_archive = True
    expect_mutable = True

    def test_current(self) -> None:
        social_contract_contents = (
            b'"Social Contract" with the Free Software Community\n'
        )
        artifact = self.playground.create_repository_index(
            "social-contract.txt", contents=social_contract_contents
        )
        self.archive.manager.add_artifact(
            artifact,
            user=self.scenario.user,
            variables={"path": "doc/social-contract.txt"},
        )
        self.archive.manager.add_artifact(
            self.playground.create_repository_index("README"),
            user=self.scenario.user,
            variables={"path": "README"},
        )

        response = self.archive_file_get(path="doc/social-contract.txt")

        self.assert_response_ok(
            response, contents=social_contract_contents, mutable=True
        )

    def test_snapshot(self) -> None:
        old_social_contract_contents = (
            b'"Social Contract" with the Free Software Community\n'
        )
        old_artifact = self.playground.create_repository_index(
            "social-contract.txt", contents=old_social_contract_contents
        )
        old_item = self.archive.manager.add_artifact(
            old_artifact,
            user=self.scenario.user,
            variables={"path": "doc/social-contract.txt"},
        )
        old_item.created_at = (timezone.now() - dt.timedelta(days=30)).replace(
            microsecond=0
        )
        old_item.save()
        new_social_contract_contents = (
            b'"Social Contract" with the Free Software Community (v2)\n'
        )
        new_artifact = self.playground.create_repository_index(
            "social-contract.txt", contents=new_social_contract_contents
        )
        new_item = self.archive.manager.add_artifact(
            new_artifact,
            user=self.scenario.user,
            variables={"path": "doc/social-contract.txt"},
            replace=True,
        )

        for snapshot, expected_contents in (
            (old_item.created_at - dt.timedelta(seconds=1), None),
            (old_item.created_at, old_social_contract_contents),
            (
                new_item.created_at - dt.timedelta(seconds=1),
                old_social_contract_contents,
            ),
            # Snapshot URLs don't include a microsecond component, so add a
            # second to avoid rounding problems.
            (
                new_item.created_at + dt.timedelta(seconds=1),
                new_social_contract_contents,
            ),
        ):
            with self.subTest(snapshot=self.format_snapshot(snapshot)):
                response = self.archive_file_get(
                    snapshot=snapshot, path="doc/social-contract.txt"
                )

                if expected_contents is None:
                    self.assertResponse404(
                        response, "No FileInArtifact matches the given query."
                    )
                else:
                    self.assert_response_ok(
                        response, contents=expected_contents, mutable=False
                    )

    def test_ignores_content_negotiation(self) -> None:
        self.archive.manager.add_artifact(
            self.playground.create_repository_index(
                "README", contents=b"Read me"
            ),
            user=self.scenario.user,
            variables={"path": "README"},
        )

        response = self.archive_file_get(
            path="README", headers={"Accept": "application/octet-stream"}
        )

        self.assert_response_ok(response, contents=b"Read me", mutable=True)


class RenderableSuiteTests(TestCase):
    """Tests for RenderableSuite."""

    @override
    def setUp(self) -> None:
        super().setUp()
        self.trixie = self.playground.create_collection(
            "trixie", CollectionCategory.SUITE
        )

    def test_as_deb822_source(self) -> None:
        renderable = RenderableSuite(
            request_scheme="http", collection=self.trixie
        )
        self.assertEqual(
            renderable.as_deb822_source(),
            "Types: deb\n"
            "URIs: http://127.0.0.1:8001/debusine/System\n"
            "Suites: trixie\n",
        )

    def test_required_suites_as_deb822_sources(self) -> None:
        personal = self.playground.create_collection(
            "personal", CollectionCategory.SUITE
        )
        trixie_backports = self.playground.create_collection(
            "trixie-backports", CollectionCategory.SUITE
        )
        trixie_security = self.playground.create_collection(
            "trixie-security", CollectionCategory.SUITE
        )
        CollectionRelation.objects.create(
            source=personal,
            target=trixie_backports,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            created_by_user=self.playground.get_default_user(),
            position=1,
        )
        CollectionRelation.objects.create(
            source=personal,
            target=trixie_security,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            created_by_user=self.playground.get_default_user(),
            position=2,
        )
        CollectionRelation.objects.create(
            source=trixie_backports,
            target=self.trixie,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            created_by_user=self.playground.get_default_user(),
            position=1,
        )
        CollectionRelation.objects.create(
            source=trixie_backports,
            target=trixie_security,
            type=CollectionRelation.Types.SUITE_REQUIRES,
            created_by_user=self.playground.get_default_user(),
            position=1,
        )

        trixie_backports_renderable = RenderableSuite(
            request_scheme="http", collection=trixie_backports
        )
        trixie_security_renderable = RenderableSuite(
            request_scheme="http", collection=trixie_security
        )
        trixie_renderable = RenderableSuite(
            request_scheme="http", collection=self.trixie
        )

        renderable = RenderableSuite(request_scheme="http", collection=personal)

        self.assertEqual(
            renderable.required_suites_as_deb822_sources(),
            f"{trixie_backports_renderable.as_deb822_source()}\n"
            f"{trixie_security_renderable.as_deb822_source()}\n"
            f"{trixie_renderable.as_deb822_source()}",
        )


# Avoid running tests from common base classes.
del ArchiveViewTests
del ArchiveFileViewTests
