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

import binascii
import hashlib
from datetime import timedelta
from pathlib import Path
from typing import ClassVar

from asgiref.sync import sync_to_async

import django.db.utils
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone

from debusine.artifacts.local_artifact import WorkRequestDebugLogs
from debusine.artifacts.models import ArtifactCategory
from debusine.db.models import (
    Artifact,
    ArtifactRelation,
    File,
    FileInArtifact,
    FileInStore,
    FileStore,
    FileUpload,
    Identity,
    NotificationChannel,
    Token,
    User,
    Worker,
    Workspace,
    default_workspace,
)
from debusine.db.tests.utils import _calculate_hash_from_data
from debusine.test import TestHelpersMixin
from debusine.test.django import ChannelsHelpersMixin
from debusine.test.utils import data_generator


class TokenTests(ChannelsHelpersMixin, TestCase):
    """Unit tests of the ``Token`` model."""

    user: ClassVar[User]

    @classmethod
    def setUpTestData(cls):
        """Set up common test data."""
        cls.user = get_user_model().objects.create_user(
            username="john", email="john@example.com"
        )

    def test_save(self):
        """The model creates a Token.key on save or keeps it if it existed."""
        token = Token.objects.create(user=self.user)

        self.assertIsNotNone(token.id)
        self.assertEqual(len(token.key), 64)

        key = token.key
        token.save()
        self.assertEqual(token.key, key)

    def test_str(self):
        """Test Token.__str__."""
        token = Token.objects.create(user=self.user)
        self.assertEqual(token.__str__(), token.hash)

    def test_user_field(self):
        """Test User field is None by default."""
        token = Token.objects.create()
        self.assertIsNone(token.user)

    def test_get_token_or_none_found(self):
        """get_token_or_none looks up a token and returns it."""
        token_hash = Token._generate_hash('some_key')
        token = Token.objects.create(hash=token_hash)

        self.assertEqual(token, Token.objects.get_token_or_none('some_key'))

    def test_get_token_or_none_not_found(self):
        """get_token_or_none cannot find a token and returns None."""
        self.assertIsNone(Token.objects.get_token_or_none('a_non_existing_key'))

    def test_enable(self):
        """enable() enables the token."""
        token = Token.objects.create()

        # Assert the default is disabled tokens
        self.assertFalse(token.enabled)

        token.enable()
        token.refresh_from_db()

        self.assertTrue(token.enabled)

    async def test_disable(self):
        """disable() disables the token."""
        token = await sync_to_async(Token.objects.create)(enabled=True)
        await sync_to_async(Worker.objects.create)(
            token=token, registered_at=timezone.now()
        )

        channel = await self.create_channel(token.hash)

        await sync_to_async(token.disable)()

        await self.assert_channel_received(channel, {"type": "worker.disabled"})
        await sync_to_async(token.refresh_from_db)()

        self.assertFalse(token.enabled)


class TokenManagerTests(TestCase):
    """Unit tests for the ``TokenManager`` class."""

    user_john: ClassVar[User]
    user_bev: ClassVar[User]
    token_john: ClassVar[Token]
    token_bev: ClassVar[Token]

    @classmethod
    def setUpTestData(cls):
        """Test data used by all the tests."""
        cls.user_john = get_user_model().objects.create_user(
            username="John", email="john@example.com"
        )
        cls.user_bev = get_user_model().objects.create_user(
            username="Bev", email="bev@example.com"
        )
        cls.token_john = Token.objects.create(user=cls.user_john)
        cls.token_bev = Token.objects.create(user=cls.user_bev)

    def test_get_tokens_all(self):
        """get_tokens returns all the tokens if no filter is applied."""
        self.assertQuerysetEqual(
            Token.objects.get_tokens(),
            {self.token_bev, self.token_john},
            ordered=False,
        )

    def test_get_tokens_by_owner(self):
        """get_tokens returns the correct tokens when filtering by owner."""
        self.assertQuerysetEqual(
            Token.objects.get_tokens(username='John'), [self.token_john]
        )
        self.assertQuerysetEqual(
            Token.objects.get_tokens(username='Bev'), [self.token_bev]
        )
        self.assertQuerysetEqual(
            Token.objects.get_tokens(username='Someone'), []
        )

    def test_get_tokens_by_key(self):
        """get_tokens returns the correct tokens when filtering by key."""
        self.assertQuerysetEqual(
            Token.objects.get_tokens(key=self.token_john.key),
            [self.token_john],
        )
        self.assertQuerysetEqual(
            Token.objects.get_tokens(key='non-existing-key'), []
        )

    def test_get_tokens_by_key_owner_empty(self):
        """
        get_tokens returns nothing if using a key and username without matches.

        Key for the key parameter or username for the user parameter exist
        but are for different tokens.
        """
        self.assertQuerysetEqual(
            Token.objects.get_tokens(
                key=self.token_john.key, username=self.user_bev.username
            ),
            [],
        )


class FileManagerTests(TestHelpersMixin, TestCase):
    """Unit tests for the ``FileManager`` class."""

    def test_create(self):
        """FileManager.create() supports the generic hash_digest field."""
        file_hash = (
            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
        )
        file = File.objects.create(
            hash_digest=binascii.unhexlify(file_hash), size=5
        )

        self.assertEqual(file.hash_digest.hex(), file_hash)


class FileTests(TestHelpersMixin, TestCase):
    """Tests for the File class."""

    def setUp(self):
        """Set up File to be used in the tests."""
        self.file_contents = b"test"
        self.file_hash = _calculate_hash_from_data(self.file_contents)

        self.file = self.create_file(self.file_contents)

    def test_sha256(self):
        """File.sha256 have the expected hash."""
        self.assertEqual(self.file.sha256, self.file_hash)
        self.assertEqual(self.file.sha256.hex(), self.file_hash.hex())

    def test_hash_digest_getter(self):
        """File.hash_digest return the expected hash digest."""
        self.assertIsInstance(self.file.hash_digest, bytes)
        self.assertEqual(self.file.hash_digest, self.file_hash)
        self.assertEqual(self.file.hash_digest.hex(), self.file_hash.hex())

    def test_hash_digest_setter(self):
        """File.hash_digest setter sets the hash digest."""
        the_hash = (
            '960fd32831fc6d6dbf5c4d141bff1a75d37bec521e52cb89e69780612a4ca04a'
        )
        self.file.hash_digest = binascii.unhexlify(the_hash)

        self.file.save()
        self.file.refresh_from_db()

        self.assertEqual(self.file.hash_digest.hex(), the_hash)

    def test_calculate_hash(self):
        """calculate_hash returns the expected hash digest."""
        file_contents = b"testing"
        local_file = self.create_temporary_file(contents=file_contents)
        expected_hash = _calculate_hash_from_data(file_contents)

        self.assertEqual(
            self.file.calculate_hash(
                local_file,
            ),
            expected_hash,
        )

    def test_str(self):
        """__str__() return sha256 and size."""
        self.assertEqual(
            str(self.file),
            f"id: {self.file.id} "
            f"sha256: {self.file_hash.hex()} "
            f"size: {len(self.file_contents)}",
        )

    def test_unique_constraint_hash_size(self):
        """File with same hash_digest and size cannot be created."""
        with self.assertRaisesRegex(
            django.db.utils.IntegrityError, "db_file_unique_sha256_size"
        ):
            File.objects.create(
                **{
                    File.current_hash_algorithm: self.file.hash_digest,
                    "size": self.file.size,
                }
            )

    def test_hash_can_be_duplicated(self):
        """File with the same hash_digest and different size can be created."""
        file_existing = File.objects.all().first()
        file_new = File.objects.create(
            hash_digest=file_existing.hash_digest, size=file_existing.size + 10
        )
        self.assertIsNotNone(file_new.id)

    def test_constraint_hash_digest_not_empty(self):
        """File.hash_digest cannot be empty."""
        with self.assertRaisesRegex(
            django.db.utils.IntegrityError, "db_file_sha256_not_empty"
        ):
            File.objects.create(hash_digest=b"", size=5)

    def test_get_or_create_not_created(self):
        """
        File.get_or_create returns created=False.

        File.get_or_create() tried to create a file with the same file_hash
        and size as one already existing.
        """
        fileobj, created = File.get_or_create(
            hash_digest=self.file_hash, size=len(self.file_contents)
        )

        self.assertEqual(self.file, fileobj)
        self.assertFalse(created)

    def test_get_or_create_created(self):
        """
        File.get_or_create returns created=True.

        File.get_or_create() created a new File (file_hash and size did not
        exist).
        """
        file_contents = self.file_contents + b"-new-contents"
        hash_digest = _calculate_hash_from_data(file_contents)
        size = len(file_contents)

        fileobj, created = File.get_or_create(
            hash_digest=hash_digest, size=size
        )

        self.assertIsInstance(fileobj, File)
        self.assertEqual(fileobj.hash_digest, hash_digest)
        self.assertEqual(fileobj.size, size)
        self.assertTrue(created)


class FileStoreTests(TestHelpersMixin, TestCase):
    """Tests for the FileStore class."""

    def setUp(self):
        """Set up FileStore to be used in the tests."""
        self.backend = FileStore.BackendChoices.LOCAL
        self.file_store_name = "nas-01"

        self.file_store = FileStore.objects.create(
            name=self.file_store_name,
            backend=self.backend,
        )

    def test_default_values(self):
        """Test default values."""
        file_store = FileStore.objects.create(
            name="nas-02", backend=FileStore.BackendChoices.LOCAL
        )
        self.assertEqual(file_store.configuration, {})
        file_store.clean_fields()
        file_store.save()

    def test_backend_choice(self):
        """Assert FileStore.BackendChoices is correctly accessed."""
        self.assertEqual(self.file_store.backend, self.backend)

    def test_configuration_validation_error(self):
        """`ValidationError` is raised if configuration is invalid."""
        file_store = FileStore.objects.create(
            name="test",
            backend=FileStore.BackendChoices.LOCAL,
            configuration=[],
        )
        with self.assertRaises(ValidationError) as raised:
            file_store.full_clean()
        self.assertEqual(
            raised.exception.message_dict,
            {"configuration": ["configuration must be a dictionary"]},
        )

        file_store.configuration = {"nonexistent": ""}
        with self.assertRaises(ValidationError) as raised:
            file_store.full_clean()
        messages = raised.exception.message_dict
        self.assertCountEqual(messages.keys(), ["configuration"])
        self.assertRegex(
            messages["configuration"][0],
            r"(?s)invalid file store configuration:.*"
            r"extra fields not permitted",
        )

    def test_files_through(self):
        """FileStore.files() return the expected file."""
        # Create a new file
        file = self.create_file()

        # Add the file in the store
        FileInStore.objects.create(file=file, store=self.file_store, data={})

        # Assert that self.file_store through model return the expected file
        self.assertEqual(self.file_store.files.first(), file)

    def test_default(self):
        """default() returns FileStore with name=="Default"."""
        self.assertEqual(FileStore.default().name, "Default")

    def test_str(self):
        """__str__() return the correct information."""
        self.assertEqual(
            self.file_store.__str__(),
            f"Id: {self.file_store.id} "
            f"Name: {self.file_store.name} "
            f"Backend: {self.file_store.backend}",
        )


class FileInStoreTests(TestCase):
    """Tests for the FileInStore class."""

    def setUp(self):
        """Set up FileInStore to be used in the tests."""
        self.file_store_name = "nas-01"
        self.file_store = FileStore.objects.create(
            name="nas-01",
            backend=FileStore.BackendChoices.LOCAL,
            configuration={},
        )

        file_contents = b"test"

        self.file_hash = hashlib.new(File.current_hash_algorithm).digest()
        self.file_length = len(file_contents)

        self.file = File.objects.create(
            hash_digest=self.file_hash, size=self.file_length
        )
        self.file_in_store = FileInStore(
            store=self.file_store,
            file=self.file,
        )

    def test_default_values(self):
        """Test default values."""
        # Delete all FileInStore to create a new one, in this test,
        # reusing self.file_store and self.file
        FileInStore.objects.all().delete()
        file_in_store = FileInStore(store=self.file_store, file=self.file)

        self.assertEqual(file_in_store.data, {})
        file_in_store.clean_fields()
        file_in_store.save()

    def test_str(self):
        """__str__() return the correct string."""
        self.assertEqual(
            self.file_in_store.__str__(),
            f"Id: {self.file_in_store.id} "
            f"Store: {self.file_in_store.store.name} "
            f"File: {self.file_in_store.file.hash_digest.hex()}",
        )


class ArtifactManagerTests(TestHelpersMixin, TestCase):
    """Tests for the ArtifactManager class."""

    def setUp(self):
        """Set up test."""
        self.workspace = Workspace.objects.create_with_name(name="test")

    def test_create_from_local_artifact(self):
        """create_from_local_artifact() creates a matching Artifact."""
        temp_dir = self.create_temporary_directory()
        paths = [temp_dir / "1.log", temp_dir / "2.log"]
        paths[0].write_text("1\n")
        paths[1].write_text("2\n")
        local_artifact = WorkRequestDebugLogs.create(files=paths)
        work_request = self.create_work_request()

        artifact = Artifact.objects.create_from_local_artifact(
            local_artifact, self.workspace, created_by_work_request=work_request
        )

        self.assertEqual(artifact.category, local_artifact.category)
        self.assertEqual(artifact.workspace, self.workspace)
        self.assertEqual(artifact.data, local_artifact.data)
        self.assertEqual(artifact.created_by_work_request, work_request)
        self.assertEqual(artifact.fileinartifact_set.count(), 2)
        file_store = self.workspace.default_file_store.get_backend_object()
        for file_in_artifact, path in zip(
            artifact.fileinartifact_set.order_by("id"), paths
        ):
            self.assertEqual(file_in_artifact.path, path.name)
            with file_store.get_stream(file_in_artifact.file) as file:
                self.assertEqual(file.read(), path.read_bytes())

    def test_not_expired_return_not_expired_artifacts(self):
        """not_expired() return only the not expired artifacts."""
        artifact_1 = Artifact.objects.create(
            workspace=self.workspace, expiration_delay=timedelta(days=1)
        )
        artifact_1.created_at = timezone.now() - timedelta(days=2)
        artifact_1.save()
        artifact_2 = Artifact.objects.create(
            workspace=self.workspace, expiration_delay=timedelta(days=1)
        )
        artifact_3 = Artifact.objects.create(
            workspace=self.workspace, expiration_delay=timedelta(0)
        )

        self.assertQuerysetEqual(
            Artifact.objects.not_expired(timezone.now()),
            {artifact_2, artifact_3},
            ordered=False,
        )

    def test_expired_return_expired_artifacts(self):
        """expired() return only the expired artifacts."""
        artifact_1 = Artifact.objects.create(
            workspace=self.workspace, expiration_delay=timedelta(days=1)
        )
        artifact_1.created_at = timezone.now() - timedelta(days=2)
        artifact_1.save()

        Artifact.objects.create(
            workspace=self.workspace, expiration_delay=timedelta(days=1)
        )
        Artifact.objects.create(
            workspace=self.workspace, expiration_delay=timedelta(0)
        )

        self.assertQuerysetEqual(
            Artifact.objects.expired(timezone.now()), {artifact_1}
        )


class ArtifactTests(TestCase):
    """Tests for the Artifact class."""

    def test_default_values_of_fields(self):
        """Define the fields of the models and test their default values."""
        artifact = Artifact()

        self.assertEqual(artifact.category, "")

        artifact.category = "sample-type"

        self.assertEqual(artifact.data, {})

        artifact.workspace = Workspace.objects.create_with_name(name="test")

        artifact.clean_fields()

        date_time_before_save = timezone.now()

        artifact.save()

        # date_time_before_save <= artifact.created_at <= timezone.now()
        self.assertLessEqual(date_time_before_save, artifact.created_at)
        self.assertLessEqual(artifact.created_at, timezone.now())

        # By default, the expiration_delay is None
        self.assertIsNone(artifact.expiration_delay)

        # By default, the created_by_work_request is None
        self.assertIsNone(artifact.created_by_work_request)

        # By default, the created_by is None
        self.assertIsNone(artifact.created_by)

    def test_expired(self):
        """Test Artifact.expired() method."""
        artifact = Artifact()

        artifact.created_at = timezone.now() - timedelta(days=2)
        artifact.expiration_delay = timedelta(days=1)
        self.assertTrue(artifact.expired(timezone.now()))

        artifact.expiration_delay = timedelta(0)
        self.assertFalse(artifact.expired(timezone.now()))

        artifact.created_at = timezone.now()
        artifact.expiration_delay = timedelta(days=1)
        self.assertFalse(artifact.expired(timezone.now()))

    def test_default_expiration(self):
        """Test workspace's default_expiration_delay."""
        default_ws = default_workspace()
        self.assertEqual(default_ws.default_expiration_delay, timedelta(0))
        artifact = Artifact.objects.create(workspace=default_ws)
        self.assertIsNone(artifact.expiration_delay)

        expiring_ws = Workspace.objects.create_with_name(name="test")
        expiring_ws.default_expiration_delay = timedelta(days=7)
        test_creation_time = timezone.now()
        artifact = Artifact.objects.create(workspace=expiring_ws)
        delta = expiring_ws.default_expiration_delay
        self.assertEqual(
            (artifact.expire_at - test_creation_time).days, delta.days
        )

    def test_str(self):
        """Test for Artifact.__str__."""
        artifact = Artifact.objects.create(
            workspace=Workspace.objects.create_with_name(name="test")
        )
        self.assertEqual(
            artifact.__str__(),
            f"Id: {artifact.id} "
            f"Category: {artifact.category} "
            f"Workspace: {artifact.workspace.id}",
        )

    def test_data_validation_error(self):
        """Assert that ValidationError is raised if data is not valid."""
        default_ws = default_workspace()

        artifact = Artifact.objects.create(
            category=ArtifactCategory.WORK_REQUEST_DEBUG_LOGS,
            workspace=default_ws,
            data=[],
        )
        with self.assertRaises(ValidationError) as raised:
            artifact.full_clean()
        self.assertEqual(
            raised.exception.message_dict,
            {"data": ["data must be a dictionary"]},
        )

        artifact.category = "missing:does-not-exist"
        artifact.data = {}
        with self.assertRaises(ValidationError) as raised:
            artifact.full_clean()
        self.assertEqual(
            raised.exception.message_dict,
            {"category": ["missing:does-not-exist: invalid artifact category"]},
        )

        artifact.category = ArtifactCategory.PACKAGE_BUILD_LOG
        with self.assertRaises(ValidationError) as raised:
            artifact.full_clean()
        messages = raised.exception.message_dict
        self.assertCountEqual(messages.keys(), ["data"])
        self.assertRegex(
            messages["data"][0],
            r"invalid artifact data:"
            r" 3 validation errors for DebianPackageBuildLog\n",
        )


class FileInArtifactTests(TestHelpersMixin, TestCase):
    """Tests for the FileInArtifact class."""

    def create_file_in_artifact(
        self,
        fileobj: File | None = None,
        artifact: Artifact | None = None,
        path: str | None = None,
    ) -> FileInArtifact:
        """Return FileInArtifact to be used in tests."""
        if fileobj is None:
            fileobj = self.create_file()

        if artifact is None:
            artifact, _ = self.create_artifact()

        if path is None:
            path = "/usr/bin/test"

        return FileInArtifact.objects.create(
            artifact=artifact, file=fileobj, path=path
        )

    def test_artifact_path_unique_constraint(self):
        """Test two FileInArtifact cannot have the same artifact and path."""
        artifact, _ = self.create_artifact()

        file1 = self.create_file(b"contents1")
        file2 = self.create_file(b"contents2")

        self.create_file_in_artifact(file1, artifact, "/usr/bin/test")

        with self.assertRaisesRegex(
            django.db.utils.IntegrityError,
            "db_fileinartifact_unique_artifact_path",
        ):
            self.create_file_in_artifact(file2, artifact, "/usr/bin/test")

    def test_str(self):
        """Test FileInArtifact.__str__."""
        file_in_artifact = self.create_file_in_artifact()

        self.assertEqual(
            file_in_artifact.__str__(),
            f"Id: {file_in_artifact.id} "
            f"Artifact: {file_in_artifact.artifact.id} "
            f"Path: {file_in_artifact.path} "
            f"File: {file_in_artifact.file.id}",
        )


class FileUploadTests(TestHelpersMixin, TestCase):
    """Tests for FileUpload class."""

    def setUp(self):
        """Set up basic objects for the tests."""
        self.file_upload = self.create_file_upload()
        self.artifact = self.file_upload.file_in_artifact.artifact
        self.workspace = self.artifact.workspace
        self.file_path = self.file_upload.file_in_artifact.path
        self.file_size = self.file_upload.file_in_artifact.file.size

    def test_current_size_raise_no_file_in_artifact(self):
        """Test current_size() raise ValueError (FileInArtifact not found)."""
        wrong_path = "no-exist"
        with self.assertRaisesRegex(
            ValueError,
            f'^No FileInArtifact for Artifact {self.artifact.id} '
            f'and path "{wrong_path}"$',
        ):
            FileUpload.current_size(self.artifact, wrong_path)

    def test_current_size_raise_no_fileupload_for_file_in_artifact(self):
        """Test current_size() raise ValueError (FileUpload not found)."""
        artifact = Artifact.objects.create(
            category="test", workspace=self.workspace
        )
        file_in_artifact = FileInArtifact.objects.create(
            artifact=artifact, path="something", file=self.create_file()
        )

        with self.assertRaisesRegex(
            ValueError,
            f"^No FileUpload for FileInArtifact {file_in_artifact.id}$",
        ):
            FileUpload.current_size(artifact, "something")

    def test_current_size_return_last_position_received(self):
        """Test current_size() method return size of the file."""
        write_to_position = 30
        self.file_upload.absolute_file_path().write_bytes(
            next(data_generator(write_to_position))
        )
        self.assertEqual(
            FileUpload.current_size(
                artifact=self.artifact, path_in_artifact=self.file_path
            ),
            write_to_position,
        )

    def test_no_two_fileobj_to_same_path(self):
        """Test cannot create two FileUpload with same path."""
        data = next(data_generator(self.file_size))

        file, _ = File.get_or_create(
            hash_digest=_calculate_hash_from_data(data), size=len(data)
        )

        new_file_in_artifact = FileInArtifact.objects.create(
            artifact=self.artifact, path="README-2", file=file
        )

        with self.assertRaisesRegex(
            django.db.utils.IntegrityError, "db_fileupload_path_key"
        ):
            FileUpload.objects.create(
                file_in_artifact=new_file_in_artifact,
                path=self.file_upload.path,
            )

    def test_delete(self):
        """Test FileUpload delete() try to unlink file."""
        file_path = self.file_upload.absolute_file_path()
        Path(file_path).write_bytes(next(data_generator(self.file_size)))

        self.assertTrue(file_path.exists())

        with self.captureOnCommitCallbacks(execute=True) as callbacks:
            self.file_upload.delete()

        self.assertFalse(file_path.exists())

        self.assertEqual(len(callbacks), 1)

    def test_delete_file_does_not_exist(self):
        """Test FileUpload delete() try to unlink file but did not exist."""
        file_path = self.file_upload.absolute_file_path()

        self.assertFalse(Path(file_path).exists())

        with self.captureOnCommitCallbacks(execute=True) as callbacks:
            self.file_upload.delete()

        self.assertEqual(len(callbacks), 1)

    def test_absolute_file_path(self):
        """Test absolute_file_path() is in DEBUSINE_UPLOAD_DIRECTORY."""
        self.assertEqual(
            self.file_upload.absolute_file_path(),
            Path(settings.DEBUSINE_UPLOAD_DIRECTORY) / self.file_upload.path,
        )

    def test_str(self):
        """Test __str__."""
        self.assertEqual(self.file_upload.__str__(), f"{self.file_upload.id}")


class ArtifactRelationTests(FileInArtifactTests, TestCase):
    """Implement ArtifactRelation tests."""

    def setUp(self):
        """Initialize test object."""
        self.artifact_1, _ = self.create_artifact()
        self.artifact_2 = Artifact.objects.create(
            workspace=self.artifact_1.workspace,
            category=self.artifact_1.category,
        )

    def test_type_not_valid_raise_validation_error(self):
        """Assert that ValidationError is raised if type is not valid."""
        artifact_relation = ArtifactRelation.objects.create(
            artifact=self.artifact_1,
            target=self.artifact_2,
            type="wrong-type",
        )
        with self.assertRaises(ValidationError):
            artifact_relation.full_clean()

    def test_str(self):
        """Assert __str__ return expected value."""
        artifact_relation = ArtifactRelation.objects.create(
            artifact=self.artifact_1,
            target=self.artifact_2,
            type=ArtifactRelation.Relations.RELATES_TO,
        )
        self.assertEqual(
            str(artifact_relation),
            f"{self.artifact_1.id} {ArtifactRelation.Relations.RELATES_TO} "
            f"{self.artifact_2.id}",
        )


class NotificationChannelTests(TestCase):
    """Tests for NotificationChannel class."""

    def setUp(self):
        """Set up test."""
        self.email_data = {
            "from": "sender@debusine.example.org",
            "to": ["recipient@example.com"],
        }

    def test_create_email(self):
        """Create email NotificationChannel."""
        name = "deblts-email"
        method = NotificationChannel.Methods.EMAIL

        notification_channel = NotificationChannel.objects.create(
            name=name, method=method, data=self.email_data
        )

        notification_channel.refresh_from_db()

        self.assertEqual(notification_channel.name, name)
        self.assertEqual(notification_channel.method, method)
        self.assertEqual(notification_channel.data, self.email_data)

    def test_create_email_invalid_data(self):
        """Create email NotificationChannel: invalid data, failure."""
        with self.assertRaises(ValidationError):
            NotificationChannel.objects.create(
                name="deblts-email",
                method=NotificationChannel.Methods.EMAIL,
                data={"something": "something"},
            )

    def test_name_unique(self):
        """Field name is unique."""
        name = "deblts-email"
        NotificationChannel.objects.create(
            name=name,
            method=NotificationChannel.Methods.EMAIL,
            data=self.email_data,
        )

        with self.assertRaises(ValidationError):
            NotificationChannel.objects.create(
                name=name,
                method=NotificationChannel.Methods.EMAIL,
                data=self.email_data,
            )

    def test_str(self):
        """Assert __str__ return the expected string."""
        name = "deblts-email"
        notification_channel = NotificationChannel.objects.create(
            name=name,
            method=NotificationChannel.Methods.EMAIL,
            data=self.email_data,
        )

        self.assertEqual(str(notification_channel), name)


class IdentityTests(TestCase):
    """Test for Identity class."""

    def test_str(self):
        """Stringification should show the unique key."""
        ident = Identity(issuer="salsa", subject="test@debian.org")
        self.assertEqual(str(ident), "salsa:test@debian.org")
