# Copyright 2019, 2021-2024 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.

"""Unit tests for the workspace models."""
from typing import ClassVar
from unittest import mock

from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import IntegrityError, transaction

from debusine.artifacts.models import CollectionCategory
from debusine.db.context import ContextConsistencyError, context
from debusine.db.models import (
    Collection,
    FileInArtifact,
    FileInStore,
    FileStore,
    Group,
    Scope,
    Workspace,
    default_workspace,
)
from debusine.db.models.auth import User
from debusine.db.models.permissions import PermissionUser
from debusine.db.models.workspaces import (
    WorkspaceChain,
    WorkspaceRole,
    is_valid_workspace_name,
)
from debusine.test.django import (
    AllowAll,
    DenyAll,
    TestCase,
    override_permission,
)


class WorkspaceManagerTests(TestCase):
    """Tests for the WorkspaceManager class."""

    def test_get_roles_model(self) -> None:
        """Test the get_roles_model method."""
        self.assertIs(Workspace.objects.get_roles_model(), WorkspaceRole)


class WorkspaceTests(TestCase):
    """Tests for the Workspace class."""

    a: ClassVar[Workspace]
    b: ClassVar[Workspace]
    c: ClassVar[Workspace]
    d: ClassVar[Workspace]
    system: ClassVar[Workspace]
    user: ClassVar[User]
    scope: ClassVar[Scope]

    @staticmethod
    def _get_collection(
        workspace: Workspace,
        name: str,
        category: CollectionCategory = CollectionCategory.SUITE,
        user: User | AnonymousUser | None = None,
    ) -> Collection:
        """Shortcut to lookup a collection from a workspace."""
        if user is None:
            user = AnonymousUser()
        return workspace.get_collection(name=name, category=category, user=user)

    @classmethod
    @context.disable_permission_checks()
    def setUpTestData(cls) -> None:
        """Set up common data for tests."""
        super().setUpTestData()
        cls.scope = cls.playground.default_scope
        cls.user = cls.playground.get_default_user()
        cls.a = cls.playground.create_workspace(name="a", public=True)
        cls.b = cls.playground.create_workspace(name="b", public=True)
        cls.c = cls.playground.create_workspace(name="c", public=True)
        cls.d = cls.playground.create_workspace(name="d", public=True)
        cls.system = cls.playground.get_default_workspace()

    def assertInherits(
        self, child: Workspace, parents: list[Workspace]
    ) -> None:
        """Ensure inheritance chain matches."""
        chain = list(
            child.chain_parents.order_by("order").values_list(
                "child__name", "parent__name", "order"
            )
        )

        expected: list[tuple[str, str, int]] = []
        for idx, parent in enumerate(parents):
            expected.append((child.name, parent.name, idx))
        self.assertEqual(chain, expected)

    def assertGetCollectionEqual(
        self,
        workspace: Workspace,
        name: str,
        expected: Collection,
        *,
        category: CollectionCategory = CollectionCategory.SUITE,
        user: User | None = None,
    ) -> None:
        """Check that collection lookup yields the given result."""
        self.assertEqual(
            self._get_collection(
                workspace, name=name, category=category, user=user
            ),
            expected,
        )

    def assertGetCollectionFails(
        self,
        workspace: Workspace,
        name: str,
        *,
        category: CollectionCategory = CollectionCategory.SUITE,
        user: User | None = None,
    ) -> None:
        """Check that collection lookup fails."""
        with self.assertRaises(Collection.DoesNotExist):
            self._get_collection(
                workspace, name=name, category=category, user=user
            )

    def test_is_valid_workspace_name(self) -> None:
        """Test is_valid_workspace_name."""
        valid_names = (
            "debian",
            "debusine",
            "System",
            "c++",
            "foo_bar",
            "foo.bar",
            "foo+bar",
            "tail_",
            "c--",
        )
        invalid_names = (
            "artifact",
            "workspaces",
            "+tag",
            "-tag",
            ".profile",
            "_reserved",
            "foo:bar",
        )

        for name in valid_names:
            with self.subTest(name=name):
                self.assertTrue(is_valid_workspace_name(name))

        for name in invalid_names:
            with self.subTest(name=name):
                self.assertFalse(is_valid_workspace_name(name))

    def test_workspace_name_validation(self) -> None:
        """Test validation for workspace names."""
        kwargs = {
            "scope": self.scope,
            "default_file_store": self.playground.get_default_file_store(),
        }
        Workspace(name="foo", **kwargs).full_clean()
        Workspace(name="foo_", **kwargs).full_clean()
        with self.assertRaises(ValidationError) as exc:
            Workspace(name="_foo", **kwargs).full_clean()
        self.assertEqual(
            exc.exception.message_dict,
            {'name': ["'_foo' is not a valid workspace name"]},
        )

    @context.disable_permission_checks()
    def test_default_values_fields(self) -> None:
        """Test basic behavior."""
        name = "test"
        workspace = Workspace(
            name=name,
            default_file_store=FileStore.default(),
            scope=self.scope,
        )

        self.assertEqual(workspace.name, name)
        self.assertFalse(workspace.public)

        workspace.clean_fields()
        workspace.save()

    @context.disable_permission_checks()
    def test_scoping(self) -> None:
        """Test scoping."""
        kwargs = {
            "default_file_store": self.playground.get_default_file_store()
        }
        scope1 = self.playground.get_or_create_scope("scope1")
        scope2 = self.playground.get_or_create_scope("scope2")
        Workspace.objects.create(name="test", scope=scope1, **kwargs)
        Workspace.objects.create(name="test", scope=scope2, **kwargs)
        with (
            transaction.atomic(),
            self.assertRaisesRegex(
                IntegrityError,
                # Only match constraint name to support non-english locales
                # "duplicate key value violates unique constraint"
                "db_workspace_unique_scope_name",
            ),
        ):
            Workspace.objects.create(name="test", scope=scope1, **kwargs)

    @context.disable_permission_checks()
    def test_is_file_in_workspace_default_file_store(self) -> None:
        """
        Test file_stores and is_file_in_workspace return the correct values.

        The file is added in the default FileStore.
        """
        fileobj = self.create_file()
        workspace = default_workspace()
        self.assertQuerySetEqual(workspace.file_stores(fileobj), [])
        self.assertFalse(workspace.is_file_in_workspace(fileobj))

        # Add file in store
        FileInStore.objects.create(
            file=fileobj, store=workspace.default_file_store
        )

        self.assertQuerySetEqual(
            workspace.file_stores(fileobj), [workspace.default_file_store]
        )
        self.assertFalse(workspace.is_file_in_workspace(fileobj))

        # Add file to an artifact in another workspace
        other_workspace = self.playground.create_workspace(name="other")
        other_artifact, _ = self.create_artifact(workspace=other_workspace)
        FileInArtifact.objects.create(
            artifact=other_artifact, path="test", file=fileobj
        )

        self.assertQuerySetEqual(
            workspace.file_stores(fileobj), [workspace.default_file_store]
        )
        self.assertFalse(workspace.is_file_in_workspace(fileobj))

        # Add different but complete file to an artifact in this workspace
        artifact, _ = self.create_artifact(workspace=workspace)
        FileInArtifact.objects.create(
            artifact=artifact,
            path="other",
            file=self.create_file(b"other"),
            complete=True,
        )

        self.assertQuerySetEqual(
            workspace.file_stores(fileobj), [workspace.default_file_store]
        )
        self.assertFalse(workspace.is_file_in_workspace(fileobj))

        # Add incomplete file to an artifact in this workspace
        file_in_artifact = FileInArtifact.objects.create(
            artifact=artifact, path="test", file=fileobj, complete=False
        )

        self.assertQuerySetEqual(
            workspace.file_stores(fileobj), [workspace.default_file_store]
        )
        self.assertFalse(workspace.is_file_in_workspace(fileobj))

        # Mark the previous file as complete
        file_in_artifact.complete = True
        file_in_artifact.save()

        self.assertQuerySetEqual(
            workspace.file_stores(fileobj), [workspace.default_file_store]
        )
        self.assertTrue(workspace.is_file_in_workspace(fileobj))

    def test_is_file_in_workspace_other_file_stores(self) -> None:
        """
        Test file_stores and is_file_in_workspace return the correct values.

        The file is added in a FileStore that is the non-default.
        """
        fileobj = self.create_file()
        workspace = default_workspace()
        self.assertQuerySetEqual(workspace.file_stores(fileobj), [])
        self.assertFalse(workspace.is_file_in_workspace(fileobj))

        # Add file in a store which is not the default one
        store = FileStore.objects.create(
            name="nas-01", backend=FileStore.BackendChoices.LOCAL
        )
        workspace.other_file_stores.add(store)
        workspace.refresh_from_db()

        self.assertQuerySetEqual(workspace.file_stores(fileobj), [])
        self.assertFalse(workspace.is_file_in_workspace(fileobj))

        # Add file in the store
        FileInStore.objects.create(file=fileobj, store=store)

        self.assertQuerySetEqual(workspace.file_stores(fileobj), [store])
        self.assertFalse(workspace.is_file_in_workspace(fileobj))

        # Add file to an artifact in this workspace
        artifact, _ = self.create_artifact(workspace=workspace)
        FileInArtifact.objects.create(
            artifact=artifact, path="test", file=fileobj, complete=True
        )

        self.assertQuerySetEqual(workspace.file_stores(fileobj), [store])
        self.assertTrue(workspace.is_file_in_workspace(fileobj))

        # Add file to the default store as well
        FileInStore.objects.create(
            file=fileobj, store=workspace.default_file_store
        )

        self.assertQuerySetEqual(
            workspace.file_stores(fileobj),
            [workspace.default_file_store, store],
        )
        self.assertTrue(workspace.is_file_in_workspace(fileobj))

    def test_str(self) -> None:
        """Test __str__ method."""
        workspace = Workspace(name="test", scope=self.scope)

        self.assertEqual(workspace.__str__(), "debusine/test")

    def test_workspacechain_str(self) -> None:
        """Test WorkspaceChain.__str__ method."""
        chain = WorkspaceChain(parent=self.a, child=self.b, order=5)
        self.assertEqual(chain.__str__(), "5:b→a")

    def test_set_inheritance(self) -> None:
        """Test set_inheritance method."""
        a, b, c = self.a, self.b, self.c

        a.set_inheritance([b])
        self.assertInherits(a, [b])
        self.assertInherits(b, [])
        self.assertInherits(c, [])

        a.set_inheritance([b, c])
        self.assertInherits(a, [b, c])
        self.assertInherits(b, [])
        self.assertInherits(c, [])

        a.set_inheritance([c, b])
        self.assertInherits(a, [c, b])
        self.assertInherits(b, [])
        self.assertInherits(c, [])

        c.set_inheritance([a])
        self.assertInherits(a, [c, b])
        self.assertInherits(b, [])
        self.assertInherits(c, [a])

        a.set_inheritance([])
        self.assertInherits(a, [])
        self.assertInherits(b, [])
        self.assertInherits(c, [a])

        with self.assertRaisesRegex(
            ValueError, r"duplicate workspace 'b' in inheritance chain"
        ):
            a.set_inheritance([b, b, c])

    def test_collection_lookup(self) -> None:
        """Test collection lookup."""
        a, b, c = self.a, self.b, self.c

        a1 = self.playground.create_collection(
            name="a", category=CollectionCategory.SUITE, workspace=a
        )

        # Lookup locally
        self.assertEqual(self._get_collection(a, "a"), a1)
        self.assertGetCollectionFails(b, "a")
        self.assertGetCollectionFails(c, "a")

        # Lookup through a simple chain
        b.set_inheritance([a])
        self.assertGetCollectionEqual(b, "a", a1)

        # Walk the inheritance chain until the end
        b.set_inheritance([c, a])
        self.assertGetCollectionEqual(b, "a", a1)
        self.assertGetCollectionFails(c, "a")

        # Create a chain loop, lookup does not break
        c.set_inheritance([b])
        self.assertGetCollectionEqual(b, "a", a1)

    def test_collection_lookup_graph(self) -> None:
        """Test collection lookup graph."""
        a, b, c, d = self.a, self.b, self.c, self.d

        cb = self.playground.create_collection(
            name="a", category=CollectionCategory.SUITE, workspace=b
        )
        cc = self.playground.create_collection(
            name="a", category=CollectionCategory.SUITE, workspace=c
        )

        # Lookup happens in order
        a.set_inheritance([b, c])
        self.assertGetCollectionEqual(a, "a", cb)
        a.set_inheritance([c, b])
        self.assertGetCollectionEqual(a, "a", cc)

        # Lookup happens depth-first
        a.set_inheritance([d, c])
        b.set_inheritance([])
        c.set_inheritance([])
        d.set_inheritance([b])
        self.assertGetCollectionEqual(a, "a", cb)

    def test_user_restrictions(self) -> None:
        """Test user restriction enforcement."""
        with context.disable_permission_checks():
            wpub = self.playground.create_workspace(name="public", public=True)
            cpub = self.playground.create_collection(
                "test", CollectionCategory.SUITE, workspace=wpub
            )
            wpriv = self.playground.create_workspace(
                name="private", public=False
            )
            cpriv = self.playground.create_collection(
                "test", CollectionCategory.SUITE, workspace=wpriv
            )
            wstart = self.playground.create_workspace(name="start", public=True)

            self.playground.create_group_role(
                wpriv, Workspace.Roles.OWNER, self.user
            )

            other_user = User.objects.create_user(
                username="other", email="other@example.org"
            )

        self.assertGetCollectionFails(wstart, "test", user=None)
        self.assertGetCollectionFails(wstart, "test", user=self.user)
        self.assertGetCollectionFails(wstart, "test", user=other_user)

        # Lookups in the workspace itself do check restrictions
        self.assertGetCollectionEqual(wpub, "test", cpub, user=None)
        self.assertGetCollectionEqual(wpub, "test", cpub, user=self.user)
        self.assertGetCollectionEqual(wpub, "test", cpub, user=other_user)
        self.assertGetCollectionFails(wpriv, "test", user=None)
        self.assertGetCollectionEqual(wpriv, "test", cpriv, user=self.user)
        self.assertGetCollectionFails(wpriv, "test", user=other_user)

        # Inheritance chain is always followed for public datasets
        wstart.set_inheritance([wpub])
        self.assertGetCollectionEqual(wstart, "test", cpub, user=None)
        self.assertGetCollectionEqual(wstart, "test", cpub, user=self.user)
        self.assertGetCollectionEqual(wstart, "test", cpub, user=other_user)

        # Inheritance chain on private datasets is followed only if logged in
        wstart.set_inheritance([wpriv])
        self.assertGetCollectionFails(wstart, "test", user=None)
        self.assertGetCollectionEqual(wstart, "test", cpriv, user=self.user)
        self.assertGetCollectionFails(wstart, "test", user=other_user)

        # Inheritance chain skips private datasets but can see public ones
        wstart.set_inheritance([wpriv, wpub])
        self.assertGetCollectionEqual(wstart, "test", cpub, user=None)
        self.assertGetCollectionEqual(wstart, "test", cpriv, user=self.user)
        self.assertGetCollectionEqual(wstart, "test", cpub, user=other_user)

    def test_assign_role(self) -> None:
        """Test assigning roles."""
        group = Group.objects.create(name="Group", scope=self.scope)
        sr = self.a.assign_role(Workspace.Roles.OWNER, group)
        self.assertEqual(sr.resource, self.a)
        self.assertEqual(sr.group, group)
        self.assertEqual(sr.role, Workspace.Roles.OWNER)

        scope1 = Scope.objects.create(name="Scope1")
        group1 = Group.objects.create(name="Group", scope=scope1)
        with self.assertRaisesRegex(
            ValueError, r"group Scope1/Group is not in scope debusine"
        ):
            self.a.assign_role(Workspace.Roles.OWNER, group1)

    def test_get_roles_not_authenticated(self) -> None:
        """Test Workspace.get_roles when not authenticated."""
        user = AnonymousUser()
        self.assertQuerySetEqual(self.a.get_roles(user), [])

    def test_get_roles(self) -> None:
        """Test Workspace.get_roles."""
        group = self.playground.create_group_role(self.a, Workspace.Roles.OWNER)
        user = self.playground.get_default_user()
        self.assertQuerySetEqual(self.a.get_roles(user), [])

        group.users.add(user)
        self.assertEqual(list(self.a.get_roles(user)), [Workspace.Roles.OWNER])

    def test_get_roles_multiple_groups(self) -> None:
        """Test Workspace.get_roles with the user in multiple groups."""
        group1 = self.playground.create_group_role(
            self.a, Workspace.Roles.OWNER, name="Group1"
        )
        group2 = self.playground.create_group_role(
            self.a, Workspace.Roles.OWNER, name="Group2"
        )

        user = self.playground.get_default_user()
        self.assertQuerySetEqual(self.a.get_roles(user), [])

        group1.users.add(user)
        self.assertEqual(list(self.a.get_roles(user)), [Workspace.Roles.OWNER])

        group2.users.add(user)
        self.assertEqual(list(self.a.get_roles(user)), [Workspace.Roles.OWNER])

    def test_in_current_scope(self) -> None:
        """Test the in_current_scope() QuerySet filter."""
        scope1 = Scope.objects.create(name="Scope1")
        self.a.scope = scope1
        self.a.save()
        self.b.scope = scope1
        self.b.save()

        context.set_scope(scope1)
        self.assertQuerySetEqual(
            Workspace.objects.in_current_scope(),
            [self.a, self.b],
            ordered=False,
        )

    def test_in_current_scope_no_context_scope(self) -> None:
        """Test the in_current_scope() QuerySet filter without scope set."""
        with self.assertRaisesRegex(
            ContextConsistencyError, "scope is not set"
        ):
            Workspace.objects.in_current_scope()

    def test_set_current_without_scope(self) -> None:
        """Test set_current without scope."""
        with self.assertRaisesRegex(
            ContextConsistencyError, "Cannot set workspace before scope"
        ):
            self.a.set_current()
        self.assertIsNone(context.workspace)
        self.assertIsNone(context._workspace_roles.get())

    def test_set_current_without_user(self) -> None:
        """Test set_current without user."""
        context.set_scope(self.scope)
        with self.assertRaisesRegex(
            ContextConsistencyError, "Cannot set workspace before user"
        ):
            self.a.set_current()
        self.assertIsNone(context.workspace)
        self.assertIsNone(context._workspace_roles.get())

    def test_set_current_with_token(self) -> None:
        """Test set_current without user."""
        context.set_scope(self.scope)
        context.set_worker_token(self.playground.create_token_enabled())
        self.a.set_current()
        self.assertEqual(context.workspace, self.a)
        self.assertEqual(context.workspace_roles, frozenset())

    def test_context_cannot_change_workspace(self) -> None:
        """Changing workspace in context is not allowed."""
        context.set_scope(self.scope)
        context.set_user(self.user)
        self.a.set_current()

        with self.assertRaisesRegex(
            ContextConsistencyError,
            "Workspace was already set to debusine/a",
        ):
            self.b.set_current()

        self.assertEqual(context.scope, self.scope)
        self.assertEqual(context.user, self.user)
        self.assertEqual(context.workspace, self.a)

    def test_set_current_wrong_scope(self) -> None:
        """Test scope/workspace.scope consistency checks."""
        scope1 = Scope.objects.create(name="scope1")
        self.a.scope = scope1
        self.a.save()

        context.set_scope(self.scope)
        context.set_user(self.user)
        with self.assertRaisesRegex(
            ContextConsistencyError,
            "workspace scope 'scope1' does not match current scope 'debusine'",
        ):
            self.a.set_current()

        self.assertEqual(context.scope, self.scope)
        self.assertEqual(context.user, self.user)
        self.assertIsNone(context.workspace)
        self.assertIsNone(context._workspace_roles.get())

    def test_set_current_forbidden_workspace(self) -> None:
        """Test set_current with an inaccessible workspace."""
        self.a.public = False
        self.a.save()

        context.set_scope(self.scope)
        context.set_user(self.user)
        with self.assertRaisesRegex(
            ContextConsistencyError,
            "User playground cannot access workspace debusine/a",
        ):
            self.a.set_current()

        self.assertEqual(context.scope, self.scope)
        self.assertEqual(context.user, self.user)
        self.assertIsNone(context.workspace)
        self.assertIsNone(context._workspace_roles.get())

    def test_can_display_public(self) -> None:
        """Test the can_display predicate on public workspaces."""
        for user in (AnonymousUser(), self.user):
            with self.subTest(user=user):
                for workspace in (self.a, self.b, self.c, self.d):
                    self.assertTrue(workspace.can_display(user))
                self.assertQuerySetEqual(
                    Workspace.objects.can_display(user=user),
                    [self.a, self.b, self.c, self.d, self.system],
                    ordered=False,
                )

    def test_can_display_current(self) -> None:
        """Test the can_display predicate on the current workspace."""
        user1 = self.playground.create_user("test1")
        self.a.public = False
        self.a.save()
        self.playground.create_group_role(
            self.a, Workspace.Roles.OWNER, self.user, user1
        )

        context.set_scope(self.scope)
        context.set_user(self.user)
        self.a.set_current()

        # can_display with current workspace and user is shortcut
        with self.assertNumQueries(0):
            self.assertTrue(self.a.can_display(self.user))

        with self.assertNumQueries(1):
            self.assertTrue(self.a.can_display(user1))

    def test_can_display_delegate_to_scope(self) -> None:
        """Test can_display delegating to scope ownership."""
        # Make a private, with an owners group on its scope
        self.a.public = False
        self.a.save()
        scope_owners = self.playground.create_group_role(
            self.scope, Scope.Roles.OWNER
        )

        # Initially, workspace is not accessible
        for user in (AnonymousUser(), self.user):
            with self.subTest(user=user):
                self.assertFalse(self.a.can_display(user))
                self.assertQuerySetEqual(
                    Workspace.objects.can_display(user=user),
                    [self.b, self.c, self.d, self.system],
                    ordered=False,
                )

        # User becomes an owner of the _scope_
        scope_owners.users.add(self.user)

        # User can now display all the workspaces in the scope
        self.assertTrue(self.a.can_display(self.user))
        # Same, but with context set, to use shortcuts
        with context.local():
            context.reset()
            context.set_scope(self.scope)
            context.set_user(self.user)
            self.assertTrue(self.a.can_display(self.user))
        # Test filter version of the predicate
        self.assertQuerySetEqual(
            Workspace.objects.can_display(user=self.user),
            [self.a, self.b, self.c, self.d, self.system],
            ordered=False,
        )

        # AnonymousUser still cannot access
        self.assertFalse(self.a.can_display(AnonymousUser()))
        with context.local():
            context.reset()
            context.set_scope(self.scope)
            context.set_user(AnonymousUser())
            self.assertFalse(self.a.can_display(AnonymousUser()))
        self.assertQuerySetEqual(
            Workspace.objects.can_display(user=AnonymousUser()),
            [self.b, self.c, self.d, self.system],
            ordered=False,
        )

    def test_can_display_owned(self) -> None:
        """Test can_display owned workspace."""
        # Make a private, with an owners group
        self.a.public = False
        self.a.save()
        workspace_owners = self.playground.create_group_role(
            self.a, Workspace.Roles.OWNER
        )

        # Initially, workspace is not accessible
        for user in (AnonymousUser(), self.user):
            with self.subTest(user=user):
                self.assertFalse(self.a.can_display(user))
                self.assertQuerySetEqual(
                    Workspace.objects.can_display(user=user),
                    [self.b, self.c, self.d, self.system],
                    ordered=False,
                )

        # User becomes an owner of the _workspace_
        workspace_owners.users.add(self.user)

        # User can now display the workspace
        self.assertTrue(self.a.can_display(self.user))
        # Same, but with context set, to use shortcuts
        with context.local():
            context.reset()
            context.set_scope(self.scope)
            context.set_user(self.user)
            self.assertTrue(self.a.can_display(self.user))
        # Test filter version of the predicate
        self.assertQuerySetEqual(
            Workspace.objects.can_display(user=self.user),
            [self.a, self.b, self.c, self.d, self.system],
            ordered=False,
        )

        # AnonymousUser still cannot access
        self.assertFalse(self.a.can_display(AnonymousUser()))
        with context.local():
            context.reset()
            context.set_scope(self.scope)
            context.set_user(AnonymousUser())
            self.assertFalse(self.a.can_display(AnonymousUser()))
        self.assertQuerySetEqual(
            Workspace.objects.can_display(user=AnonymousUser()),
            [self.b, self.c, self.d, self.system],
            ordered=False,
        )

    def test_can_display_with_token(self) -> None:
        """Test can_display with a worker token."""
        # Make a private
        self.a.public = False
        self.a.save()

        user: PermissionUser

        # Workspace is not accessible
        for user in (AnonymousUser(), self.user):
            with self.subTest(user=user):
                self.assertFalse(self.a.can_display(user))
                self.assertQuerySetEqual(
                    Workspace.objects.can_display(user=user),
                    [self.b, self.c, self.d, self.system],
                    ordered=False,
                )

        context.set_worker_token(self.playground.create_token_enabled())

        # Workspace is now accessible also without user information
        for user in (None, AnonymousUser(), self.user):
            with self.subTest(user=user):
                self.assertTrue(self.a.can_display(user))
                self.assertQuerySetEqual(
                    Workspace.objects.can_display(user=user),
                    [self.a, self.b, self.c, self.d, self.system],
                    ordered=False,
                )

    def test_create_checks(self) -> None:
        """Test can_create_workspace hook in save."""
        kwargs = {"default_file_store": FileStore.default()}

        with mock.patch(
            "debusine.db.models.Scope.can_create_workspace"
        ) as pred:
            test1 = Workspace.objects.create(
                name="test1", scope=self.scope, **kwargs
            )
        pred.assert_called_once_with(None)
        self.assertIsNotNone(test1.pk)

        with override_permission(Scope, "can_create_workspace", AllowAll):
            test2 = Workspace.objects.create(
                name="test2", scope=self.scope, **kwargs
            )
        self.assertIsNotNone(test2.pk)

        with override_permission(Scope, "can_create_workspace", DenyAll):
            with self.assertRaisesRegex(
                PermissionDenied, r"None cannot create workspaces in debusine"
            ):
                Workspace.objects.create(
                    name="test3", scope=self.scope, **kwargs
                )
        self.assertQuerySetEqual(Workspace.objects.filter(name="test3"), [])


class WorkspaceRoleTests(TestCase):
    """Tests for the WorkspaceRole class."""

    workspace1: ClassVar[Workspace]
    workspace2: ClassVar[Workspace]
    group1: ClassVar[Group]
    group2: ClassVar[Group]

    @classmethod
    def setUpTestData(cls) -> None:
        """Set up common test data."""
        super().setUpTestData()
        with context.disable_permission_checks():
            cls.workspace1 = cls.playground.create_workspace(name="Workspace1")
            cls.workspace2 = cls.playground.create_workspace(name="Workspace2")
            cls.group1 = Group.objects.create(
                name="Group1", scope=cls.playground.default_scope
            )
            cls.group2 = Group.objects.create(
                name="Group2", scope=cls.playground.default_scope
            )

    def test_str(self) -> None:
        """Test stringification."""
        sr = WorkspaceRole(
            group=self.group1,
            resource=self.workspace1,
            role=WorkspaceRole.Roles.OWNER,
        )
        self.assertEqual(str(sr), "debusine/Group1─owner⟶debusine/Workspace1")

    def test_assign_multiple_groups(self) -> None:
        """Multiple group can share a role on a workspace."""
        WorkspaceRole.objects.create(
            group=self.group1,
            resource=self.workspace1,
            role=WorkspaceRole.Roles.OWNER,
        )
        WorkspaceRole.objects.create(
            group=self.group2,
            resource=self.workspace1,
            role=WorkspaceRole.Roles.OWNER,
        )

    def test_assign_multiple_scopes(self) -> None:
        """A group can be given a role in different workspaces."""
        WorkspaceRole.objects.create(
            group=self.group1,
            resource=self.workspace1,
            role=WorkspaceRole.Roles.OWNER,
        )
        WorkspaceRole.objects.create(
            group=self.group1,
            resource=self.workspace2,
            role=WorkspaceRole.Roles.OWNER,
        )
