From bdb43025214f29562be4b9ffdbb50d61b066acf6 Mon Sep 17 00:00:00 2001
From: DAVID Axel <axel.david@etu.univ-amu.fr>
Date: Sun, 7 May 2023 09:56:23 +0200
Subject: [PATCH] feat: :recycle: Add helper convert_key function

---
 src/vigenere_api/models/__init__.py           |   3 +-
 src/vigenere_api/models/helpers/__init__.py   |   6 +-
 src/vigenere_api/models/helpers/helper.py     |  45 +++
 .../models/helpers/vigenere_key.py            |  98 +++++
 src/vigenere_api/models/vigenere.py           | 221 +++++++++++
 tests/models/test_helper.py                   | 120 ++++--
 tests/models/test_vigenere.py                 | 373 ++++++++++++++++++
 tests/models/test_vigenere_key.py             |  89 +++++
 8 files changed, 910 insertions(+), 45 deletions(-)
 create mode 100644 src/vigenere_api/models/helpers/vigenere_key.py
 create mode 100644 src/vigenere_api/models/vigenere.py
 create mode 100644 tests/models/test_vigenere.py
 create mode 100644 tests/models/test_vigenere_key.py

diff --git a/src/vigenere_api/models/__init__.py b/src/vigenere_api/models/__init__.py
index c346974..e777b0f 100644
--- a/src/vigenere_api/models/__init__.py
+++ b/src/vigenere_api/models/__init__.py
@@ -17,6 +17,7 @@
 """All models used by VigenereAPI."""
 
 from .caesar import CaesarData
+from .vigenere import VigenereData
 
 
-__all__ = ["CaesarData"]
+__all__ = ["CaesarData", "VigenereData"]
diff --git a/src/vigenere_api/models/helpers/__init__.py b/src/vigenere_api/models/helpers/__init__.py
index eb8d6ae..35481ef 100644
--- a/src/vigenere_api/models/helpers/__init__.py
+++ b/src/vigenere_api/models/helpers/__init__.py
@@ -16,7 +16,7 @@
 
 """Helper package for models."""
 
-from .helper import move_char
+from .helper import convert_key, move_char
+from .vigenere_key import VigenereKey
 
-
-__all__ = ["move_char"]
+__all__ = ["move_char", "convert_key", "VigenereKey"]
diff --git a/src/vigenere_api/models/helpers/helper.py b/src/vigenere_api/models/helpers/helper.py
index 0a5c41f..a91e933 100644
--- a/src/vigenere_api/models/helpers/helper.py
+++ b/src/vigenere_api/models/helpers/helper.py
@@ -19,12 +19,17 @@
 from typing import Literal
 
 from .errors import (
+    BadKeyError,
+    EmptyKeyError,
+    ExpectedKeyType,
     HelperBadCharValueError,
     HelperBadFirstLetterValueError,
     HelperBadLengthCharValueError,
     HelperCharTypeError,
     HelperFirstLetterTypeError,
     HelperKeyTypeError,
+    KeyTypeError,
+    TooLongKeyError,
 )
 
 
@@ -80,3 +85,43 @@ def move_char(char: str, key: int, first_letter: Literal["a", "A"]) -> str:
         raise HelperBadFirstLetterValueError(first_letter)
 
     return chr((ord(char) - ord(first_letter) + key) % 26 + ord(first_letter))
+
+
+def convert_key(key: str) -> int:
+    """
+    Convert the one character string into an integer between 0 and 25.
+
+    Parameters
+    ----------
+    key : str
+        The key to convert.
+
+    Raises
+    ------
+    KeyTypeError
+        Thrown if 'key' is not a string.
+    EmptyKeyError
+        Thrown if 'key' is an empty string.
+    TooLongKeyError
+        Thrown if 'key' is too long.
+    BadKeyError
+        Thrown if 'key' is not a one alphabetical character.
+
+    Returns
+    -------
+    key_converted
+        int
+    """
+    if not isinstance(key, str):
+        raise KeyTypeError(key, ExpectedKeyType.STRING)
+
+    if len(key) == 0:
+        raise EmptyKeyError
+
+    if len(key) > 1:
+        raise TooLongKeyError
+
+    if not key.isalpha():
+        raise BadKeyError(key, ExpectedKeyType.STRING)
+
+    return ord(key) - ord("A") if key.isupper() else ord(key) - ord("a")
diff --git a/src/vigenere_api/models/helpers/vigenere_key.py b/src/vigenere_api/models/helpers/vigenere_key.py
new file mode 100644
index 0000000..db5f46b
--- /dev/null
+++ b/src/vigenere_api/models/helpers/vigenere_key.py
@@ -0,0 +1,98 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  Vigenere-API                                                                        +
+#  Copyright (C) 2023 Axel DAVID                                                       +
+#                                                                                      +
+#  This program is free software: you can redistribute it and/or modify it under       +
+#  the terms of the GNU General Public License as published by the Free Software       +
+#  Foundation, either version 3 of the License, or (at your option) any later version. +
+#                                                                                      +
+#  This program is distributed in the hope that it will be useful, but WITHOUT ANY     +
+#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR       +
+#  A PARTICULAR PURPOSE. See the GNU General Public License for more details.          +
+#                                                                                      +
+#  You should have received a copy of the GNU General Public License along with        +
+#  this program.  If not, see <https://www.gnu.org/licenses/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+"""Utils class for VigenereData."""
+
+from typing import final, Final
+
+from .errors import (
+    BadKeyError,
+    EmptyKeyError,
+    ExpectedKeyType,
+    KeyTypeError,
+    TooShortKeyError,
+)
+
+
+@final
+class VigenereKey:
+    """Util class to loop on each character of the key."""
+
+    def __init__(self, key: str) -> None:
+        """
+        Create a new Vigenere key.
+
+        Parameters
+        ----------
+        key : str
+            The string used like a key.
+
+        Raises
+        ------
+        KeyTypeError
+            Thrown if 'key' is not a string.
+        EmptyKeyError
+            Thrown if 'key' is empty.
+        TooShortKeyError
+            Thrown if 'key' is too short.
+        BadKeyError
+            Thrown if 'key' contains invalid characters.
+        """
+
+        if not isinstance(key, str):
+            raise KeyTypeError(key, ExpectedKeyType.STRING)
+
+        if len(key) == 0:
+            raise EmptyKeyError
+
+        if len(key) == 1:
+            raise TooShortKeyError
+
+        if not key.isalpha():
+            raise BadKeyError(key, ExpectedKeyType.STRING)
+
+        self.__index = 0
+        self.__key: Final = key
+
+    def __next__(self) -> str:
+        """
+        Get the next character of the key.
+
+        Returns
+        -------
+        The next character of the key.
+            str
+        """
+        try:
+            return self.__key[self.__index]
+        finally:
+            self.__increase_index()
+
+    def __increase_index(self) -> None:
+        """Increase the index of the key."""
+        self.__index += 1
+        self.__index %= len(self)
+
+    def __len__(self) -> int:
+        """
+        Get the length of the key.
+
+        Returns
+        -------
+        int
+            The length of the key.
+        """
+        return len(self.__key)
diff --git a/src/vigenere_api/models/vigenere.py b/src/vigenere_api/models/vigenere.py
new file mode 100644
index 0000000..17ca2aa
--- /dev/null
+++ b/src/vigenere_api/models/vigenere.py
@@ -0,0 +1,221 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  Vigenere-API                                                                        +
+#  Copyright (C) 2023 Axel DAVID                                                       +
+#                                                                                      +
+#  This program is free software: you can redistribute it and/or modify it under       +
+#  the terms of the GNU General Public License as published by the Free Software       +
+#  Foundation, either version 3 of the License, or (at your option) any later version. +
+#                                                                                      +
+#  This program is distributed in the hope that it will be useful, but WITHOUT ANY     +
+#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR       +
+#  A PARTICULAR PURPOSE. See the GNU General Public License for more details.          +
+#                                                                                      +
+#  You should have received a copy of the GNU General Public License along with        +
+#  this program.  If not, see <https://www.gnu.org/licenses/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+"""Caesar model."""
+
+from __future__ import annotations
+
+from enum import auto, unique
+from typing import final
+
+from pydantic import StrictStr, validator
+
+from strenum import StrEnum
+from vigenere_api.helpers import Model
+from .errors import (
+    AlgorithmExpectedKeyType,
+    AlgorithmKeyTypeError,
+    AlgorithmOperationTypeError,
+    AlgorithmTextTypeError,
+    ContentTypeError,
+    EmptyContentError,
+)
+from .helpers import convert_key, move_char, VigenereKey
+
+
+@final
+@unique
+class VigenereOperation(StrEnum):
+    """All possible Vigenere operations."""
+
+    CIPHER = auto()
+    DECIPHER = auto()
+
+
+@final
+class VigenereData(Model):
+    """
+    Vigenere data to cipher the content or decipher.
+
+    Exemples
+    --------
+    Basic example
+
+    >>> from vigenere_api.models import VigenereData
+
+    >>> vigenere_data = VigenereData(content="Hello World", key="test")
+
+    >>> ciphered_data = vigenere_data.cipher()
+    >>> deciphered_data = vigenere_data.decipher()
+
+    >>> assert vigenere_data == deciphered_data == "Hello World"
+    >>> assert vigenere_data.key == ciphered_data.key == deciphered_data.key == "test"
+    """
+
+    content: StrictStr
+    """The content to be ciphered or deciphered."""
+
+    key: StrictStr
+    """The key to cipher or decipher the content."""
+
+    def cipher(self) -> VigenereData:
+        """
+        Cipher the content with the key.
+
+        Returns
+        -------
+        ciphered_data
+            VigenereData
+        """
+        return VigenereData(
+            content=self.__algorithm(
+                self.content,
+                VigenereKey(self.key),
+                VigenereOperation.CIPHER,
+            ),
+            key=self.key,
+        )
+
+    def decipher(self) -> VigenereData:
+        """
+        Decipher the content with the key.
+
+        Returns
+        -------
+        deciphered_data
+            VigenereData
+        """
+        return VigenereData(
+            content=self.__algorithm(
+                self.content,
+                VigenereKey(self.key),
+                VigenereOperation.DECIPHER,
+            ),
+            key=self.key,
+        )
+
+    @staticmethod
+    def __algorithm(text: str, key: VigenereKey, operation: VigenereOperation) -> str:
+        """
+        Apply the common algorithm for Vigenere.
+
+        Parameters
+        ----------
+        text : str
+            The text to apply the algorithm.
+        key : VigenereKey
+            The key used by the algorithm.
+        operation : VigenereOperation
+            The wanted operation.
+
+        Raises
+        ------
+        AlgorithmTextTypeError
+            Thrown if 'text' is not a string.
+        AlgorithmKeyTypeError
+            Thrown if 'key' is not a VigenereKey object.
+        AlgorithmOperationTypeError
+            Thrown if 'operation' is not a VigenereOperation object.
+
+        Returns
+        -------
+        converted_text
+            str
+        """
+        if not isinstance(text, str):
+            raise AlgorithmTextTypeError(text)
+
+        if not isinstance(key, VigenereKey):
+            raise AlgorithmKeyTypeError(key, AlgorithmExpectedKeyType.VIGENERE_KEY)
+
+        if not isinstance(operation, VigenereOperation):
+            raise AlgorithmOperationTypeError(operation)
+
+        result = ""
+        for char in text:
+            if char.isalpha():
+                index_key = convert_key(next(key))
+                if operation == VigenereOperation.DECIPHER:
+                    index_key = -index_key
+
+                if char.isupper():
+                    result += move_char(char, index_key, "A")
+                else:
+                    result += move_char(char, index_key, "a")
+            else:
+                result += char
+
+        return result
+
+    @validator("content", pre=True)
+    def validate_content(cls, content: str) -> str:
+        """
+        Check if the affectation to content respects contraints.
+
+        Parameters
+        ----------
+        content : str
+            The new content.
+
+        Raises
+        ------
+        ContentTypeError
+            Thrown if 'content' is not a string.
+        EmptyContentError
+            Thrown if 'content' is an empty string.
+
+        Returns
+        -------
+        content
+            str
+        """
+        if not isinstance(content, str):
+            raise ContentTypeError(content)
+
+        if len(content) == 0:
+            raise EmptyContentError
+
+        return content
+
+    @validator("key", pre=True)
+    def validate_key(cls, key: str) -> str:
+        """
+        Check if the affectation to key respects contraints.
+
+        Parameters
+        ----------
+        key : str
+            The new key.
+
+        Raises
+        ------
+        KeyTypeError
+            Thrown if 'key' is not a string.
+        EmptyKeyError
+            Thrown if 'key' is an empty string.
+        TooShortKeyError
+            Thrown if 'key' is a string with a length of 1.
+        BadKeyError
+            Thrown if 'key' is not an alphabetic character.
+
+        Returns
+        -------
+        key
+            str
+        """
+        _key_can_be_instantiate = VigenereKey(key)
+
+        return key
diff --git a/tests/models/test_helper.py b/tests/models/test_helper.py
index d1f30f7..0b35705 100644
--- a/tests/models/test_helper.py
+++ b/tests/models/test_helper.py
@@ -18,54 +18,92 @@
 
 import pytest
 
-from vigenere_api.models.helpers import move_char
+from vigenere_api.models.helpers import convert_key, move_char
 from vigenere_api.models.helpers.errors import (
+    BadKeyError,
+    EmptyKeyError,
     HelperBadCharValueError,
     HelperBadFirstLetterValueError,
     HelperBadLengthCharValueError,
     HelperCharTypeError,
     HelperFirstLetterTypeError,
     HelperKeyTypeError,
+    KeyTypeError,
+    TooLongKeyError,
 )
 
 
-def test_move_lower_letter() -> None:
-    moved_letter = move_char("a", 2, "a")
-
-    assert moved_letter == "c"
-
-
-def test_move_upper_letter() -> None:
-    moved_letter = move_char("A", 2, "A")
-
-    assert moved_letter == "C"
-
-
-@pytest.mark.raises(exception=HelperCharTypeError)
-def test_bad_type_char() -> None:
-    _ignored = move_char(b"r", 2, "a")
-
-
-@pytest.mark.raises(exception=HelperBadLengthCharValueError)
-def test_bad_length_char() -> None:
-    _ignored = move_char("rr", 2, "a")
-
-
-@pytest.mark.raises(exception=HelperBadCharValueError)
-def test_bad_alpha_char() -> None:
-    _ignored = move_char("+", 2, "a")
-
-
-@pytest.mark.raises(exception=HelperKeyTypeError)
-def test_bad_type_key() -> None:
-    _ignored = move_char("a", "v", "a")
-
-
-@pytest.mark.raises(exception=HelperFirstLetterTypeError)
-def test_bad_type_first_letter() -> None:
-    _ignored = move_char("a", 2, b"a")
-
-
-@pytest.mark.raises(exception=HelperBadFirstLetterValueError)
-def test_bad_first_letter_value() -> None:
-    _ignored = move_char("a", 2, "g")
+class MoveCharSuite:
+    @staticmethod
+    def test_move_lower_letter() -> None:
+        moved_letter = move_char("a", 2, "a")
+
+        assert moved_letter == "c"
+
+    @staticmethod
+    def test_move_upper_letter() -> None:
+        moved_letter = move_char("A", 2, "A")
+
+        assert moved_letter == "C"
+
+    @staticmethod
+    @pytest.mark.raises(exception=HelperCharTypeError)
+    def test_bad_type_char() -> None:
+        _ignored = move_char(b"r", 2, "a")
+
+    @staticmethod
+    @pytest.mark.raises(exception=HelperBadLengthCharValueError)
+    def test_bad_length_char() -> None:
+        _ignored = move_char("rr", 2, "a")
+
+    @staticmethod
+    @pytest.mark.raises(exception=HelperBadCharValueError)
+    def test_bad_alpha_char() -> None:
+        _ignored = move_char("+", 2, "a")
+
+    @staticmethod
+    @pytest.mark.raises(exception=HelperKeyTypeError)
+    def test_bad_type_key() -> None:
+        _ignored = move_char("a", "v", "a")
+
+    @staticmethod
+    @pytest.mark.raises(exception=HelperFirstLetterTypeError)
+    def test_bad_type_first_letter() -> None:
+        _ignored = move_char("a", 2, b"a")
+
+    @staticmethod
+    @pytest.mark.raises(exception=HelperBadFirstLetterValueError)
+    def test_bad_first_letter_value() -> None:
+        _ignored = move_char("a", 2, "g")
+
+
+class ConvertKeySuite:
+    @staticmethod
+    def test_convert_lower_key() -> None:
+        lower_index = convert_key("a")
+        assert lower_index == 0
+
+    @staticmethod
+    def test_convert_upper_key() -> None:
+        upper_index = convert_key("A")
+        assert upper_index == 0
+
+    @staticmethod
+    @pytest.mark.raises(exception=KeyTypeError)
+    def test_bad_type_key() -> None:
+        _ignored = convert_key(b"ter")
+
+    @staticmethod
+    @pytest.mark.raises(exception=BadKeyError)
+    def test_bad_key() -> None:
+        _ignored = convert_key("8")
+
+    @staticmethod
+    @pytest.mark.raises(exception=EmptyKeyError)
+    def test_empty_key() -> None:
+        _ignored = convert_key("")
+
+    @staticmethod
+    @pytest.mark.raises(exception=TooLongKeyError)
+    def test_too_long_key() -> None:
+        _ignored = convert_key("aa")
diff --git a/tests/models/test_vigenere.py b/tests/models/test_vigenere.py
new file mode 100644
index 0000000..059813f
--- /dev/null
+++ b/tests/models/test_vigenere.py
@@ -0,0 +1,373 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  Vigenere-API                                                                        +
+#  Copyright (C) 2023 Axel DAVID                                                       +
+#                                                                                      +
+#  This program is free software: you can redistribute it and/or modify it under       +
+#  the terms of the GNU General Public License as published by the Free Software       +
+#  Foundation, either version 3 of the License, or (at your option) any later version. +
+#                                                                                      +
+#  This program is distributed in the hope that it will be useful, but WITHOUT ANY     +
+#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR       +
+#  A PARTICULAR PURPOSE. See the GNU General Public License for more details.          +
+#                                                                                      +
+#  You should have received a copy of the GNU General Public License along with        +
+#  this program.  If not, see <https://www.gnu.org/licenses/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+"""Vigenere model tests."""
+
+import pytest
+from pydantic import ValidationError
+
+from vigenere_api.models import VigenereData
+from vigenere_api.models.errors import (
+    AlgorithmKeyTypeError,
+    AlgorithmOperationTypeError,
+    AlgorithmTextTypeError,
+)
+from vigenere_api.models.vigenere import VigenereKey, VigenereOperation
+
+
+class CtorSuite:
+    @staticmethod
+    def test_with_key() -> None:
+        text = "Test"
+        key = "zz"
+        data = VigenereData(content=text, key=key)
+
+        assert data.content == text
+        assert data.key == key
+
+    @staticmethod
+    @pytest.mark.raises(exception=ValidationError)
+    def test_missing_content() -> None:
+        key = "zz"
+        _ignored_data = VigenereData(key=key)
+
+    @staticmethod
+    @pytest.mark.raises(exception=ValidationError)
+    def test_missing_key() -> None:
+        text = "z"
+        _ignored_data = VigenereData(content=text)
+
+    @staticmethod
+    @pytest.mark.raises(exception=ValidationError)
+    def test_bad_type_content() -> None:
+        text = b"Test"
+        key = "zz"
+        _ignored_data = VigenereData(content=text, key=key)
+
+    @staticmethod
+    @pytest.mark.raises(exception=ValidationError)
+    def test_bad_empty_content() -> None:
+        text = ""
+        key = "zz"
+        _ignored_data = VigenereData(content=text, key=key)
+
+    @staticmethod
+    @pytest.mark.raises(exception=ValidationError)
+    def test_bad_type_key() -> None:
+        text = "Test"
+        key = b"z"
+        _ignored_data = VigenereData(content=text, key=key)
+
+    @staticmethod
+    @pytest.mark.raises(exception=ValidationError)
+    def test_bad_empty_key() -> None:
+        text = "Test"
+        key = ""
+        _ignored_data = VigenereData(content=text, key=key)
+
+    @staticmethod
+    @pytest.mark.raises(exception=ValidationError)
+    def test_too_short_key() -> None:
+        text = "Test"
+        key = "e"
+        _ignored_data = VigenereData(content=text, key=key)
+
+    @staticmethod
+    @pytest.mark.raises(exception=ValidationError)
+    def test_bad_not_alpha_str_key() -> None:
+        text = "Test"
+        key = "$z"
+        _ignored_data = VigenereData(content=text, key=key)
+
+
+class OperationSuite:
+    class CipherSuite:
+        class SimpleKeySuite:
+            @staticmethod
+            def test_with_str_lower_key() -> None:
+                text = "Test"
+                key = "bb"
+                data = VigenereData(content=text, key=key)
+
+                assert data.content == text
+                assert data.key == key
+
+                cipher = data.cipher()
+
+                ciphered_text = "Uftu"
+                assert cipher.content == ciphered_text
+                assert cipher.key == key
+
+            @staticmethod
+            def test_with_str_upper_key() -> None:
+                text = "Test"
+                key = "BB"
+                data = VigenereData(content=text, key=key)
+
+                assert data.content == text
+                assert data.key == key
+
+                cipher = data.cipher()
+
+                ciphered_text = "Uftu"
+                assert cipher.content == ciphered_text
+                assert cipher.key == key
+
+            @staticmethod
+            def test_with_str_mixed_key() -> None:
+                text = "Test"
+                key = "Bb"
+                data = VigenereData(content=text, key=key)
+
+                assert data.content == text
+                assert data.key == key
+
+                cipher = data.cipher()
+
+                ciphered_text = "Uftu"
+                assert cipher.content == ciphered_text
+                assert cipher.key == key
+
+            @staticmethod
+            def test_equality_between_keys() -> None:
+                text = "Test"
+                data1 = VigenereData(content=text, key="Bb")
+                data2 = VigenereData(content=text, key="bb")
+                data3 = VigenereData(content=text, key="BB")
+
+                ciphered1 = data1.cipher()
+                ciphered2 = data2.cipher()
+                ciphered3 = data3.cipher()
+
+                assert data1.content == data2.content == data3.content
+                assert ciphered1.content == ciphered2.content == ciphered3.content
+
+        class ComplexKeySuite:
+            @staticmethod
+            def test_with_str_lower_key() -> None:
+                text = "Test"
+                key = "abcd"
+                data = VigenereData(content=text, key=key)
+
+                assert data.content == text
+                assert data.key == key
+
+                cipher = data.cipher()
+
+                ciphered_text = "Tfuw"
+                assert cipher.content == ciphered_text
+                assert cipher.key == key
+
+            @staticmethod
+            def test_with_str_upper_key() -> None:
+                text = "Test"
+                key = "ABCD"
+                data = VigenereData(content=text, key=key)
+
+                assert data.content == text
+                assert data.key == key
+
+                cipher = data.cipher()
+
+                ciphered_text = "Tfuw"
+                assert cipher.content == ciphered_text
+                assert cipher.key == key
+
+            @staticmethod
+            def test_with_str_mixed_key() -> None:
+                text = "Test"
+                key = "aBcD"
+                data = VigenereData(content=text, key=key)
+
+                assert data.content == text
+                assert data.key == key
+
+                cipher = data.cipher()
+
+                ciphered_text = "Tfuw"
+                assert cipher.content == ciphered_text
+                assert cipher.key == key
+
+            @staticmethod
+            def test_equality_between_keys() -> None:
+                text = "Test"
+                data1 = VigenereData(content=text, key="aBcD")
+                data2 = VigenereData(content=text, key="abcd")
+                data3 = VigenereData(content=text, key="ABCD")
+
+                ciphered1 = data1.cipher()
+                ciphered2 = data2.cipher()
+                ciphered3 = data3.cipher()
+
+                assert data1.content == data2.content == data3.content
+                assert ciphered1.content == ciphered2.content == ciphered3.content
+
+    class DecipherSuite:
+        class SimpleKeySuite:
+            @staticmethod
+            def test_with_str_lower_key() -> None:
+                text = "Test"
+                key = "bb"
+                data = VigenereData(content=text, key=key)
+
+                assert data.content == text
+                assert data.key == key
+
+                decipher = data.decipher()
+
+                deciphered_text = "Sdrs"
+                assert decipher.content == deciphered_text
+                assert decipher.key == key
+
+            @staticmethod
+            def test_with_str_upper_key() -> None:
+                text = "Test ui"
+                key = "BB"
+                data = VigenereData(content=text, key=key)
+
+                assert data.content == text
+                assert data.key == key
+
+                decipher = data.decipher()
+
+                deciphered_text = "Sdrs th"
+                assert decipher.content == deciphered_text
+                assert decipher.key == key
+
+            @staticmethod
+            def test_with_str_mixed_key() -> None:
+                text = "Test ui"
+                key = "Bb"
+                data = VigenereData(content=text, key=key)
+
+                assert data.content == text
+                assert data.key == key
+
+                decipher = data.decipher()
+
+                deciphered_text = "Sdrs th"
+                assert decipher.content == deciphered_text
+                assert decipher.key == key
+
+            @staticmethod
+            def test_equality_between_keys() -> None:
+                text = "Test"
+                data1 = VigenereData(content=text, key="Bb")
+                data2 = VigenereData(content=text, key="bb")
+                data3 = VigenereData(content=text, key="BB")
+
+                deciphered1 = data1.decipher()
+                deciphered2 = data2.decipher()
+                deciphered3 = data3.decipher()
+
+                assert data1.content == data2.content == data3.content
+                assert deciphered1.content == deciphered2.content == deciphered3.content
+
+        class ComplexKeySuite:
+            @staticmethod
+            def test_with_str_lower_key() -> None:
+                text = "Test"
+                key = "abcd"
+                data = VigenereData(content=text, key=key)
+
+                assert data.content == text
+                assert data.key == key
+
+                decipher = data.decipher()
+
+                deciphered_text = "Tdqq"
+                assert decipher.content == deciphered_text
+                assert decipher.key == key
+
+            @staticmethod
+            def test_with_str_upper_key() -> None:
+                text = "Test ui"
+                key = "ABCD"
+                data = VigenereData(content=text, key=key)
+
+                assert data.content == text
+                assert data.key == key
+
+                decipher = data.decipher()
+
+                deciphered_text = "Tdqq uh"
+                assert decipher.content == deciphered_text
+                assert decipher.key == key
+
+            @staticmethod
+            def test_with_str_mixed_key() -> None:
+                text = "Test ui"
+                key = "aBcD"
+                data = VigenereData(content=text, key=key)
+
+                assert data.content == text
+                assert data.key == key
+
+                decipher = data.decipher()
+
+                deciphered_text = "Tdqq uh"
+                assert decipher.content == deciphered_text
+                assert decipher.key == key
+
+            @staticmethod
+            def test_equality_between_keys() -> None:
+                text = "Test"
+                data1 = VigenereData(content=text, key="aBcD")
+                data2 = VigenereData(content=text, key="abcd")
+                data3 = VigenereData(content=text, key="ABCD")
+
+                deciphered1 = data1.decipher()
+                deciphered2 = data2.decipher()
+                deciphered3 = data3.decipher()
+
+                assert data1.content == data2.content == data3.content
+                assert deciphered1.content == deciphered2.content == deciphered3.content
+
+
+class InternalSuite:
+    class AlgorithmSuite:
+        algo_func = VigenereData._VigenereData__algorithm
+
+        @classmethod
+        @pytest.mark.raises(exception=AlgorithmKeyTypeError)
+        def test_with_bad_key(cls) -> None:
+            _ignored = cls.algo_func("test", "A", VigenereOperation.CIPHER)
+
+        @classmethod
+        @pytest.mark.raises(exception=AlgorithmTextTypeError)
+        def test_with_bad_text(cls) -> None:
+            _ignored = cls.algo_func(b"test", 10, VigenereOperation.DECIPHER)
+
+        @classmethod
+        @pytest.mark.raises(exception=AlgorithmOperationTypeError)
+        def test_with_bad_operation(cls) -> None:
+            _ignored = cls.algo_func("test", VigenereKey("test"), b"CIPEHR")
+
+        @classmethod
+        def test_cipher(cls) -> None:
+            ciphered_text = cls.algo_func(
+                "teSt uio", VigenereKey("test"), VigenereOperation.CIPHER
+            )
+
+            assert ciphered_text == "miKm nmg"
+
+        @classmethod
+        def test_decipher(cls) -> None:
+            ciphered_text = cls.algo_func(
+                "teSt uio", VigenereKey("test"), VigenereOperation.DECIPHER
+            )
+
+            assert ciphered_text == "aaAa bew"
diff --git a/tests/models/test_vigenere_key.py b/tests/models/test_vigenere_key.py
new file mode 100644
index 0000000..c9da6a0
--- /dev/null
+++ b/tests/models/test_vigenere_key.py
@@ -0,0 +1,89 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  Vigenere-API                                                                        +
+#  Copyright (C) 2023 Axel DAVID                                                       +
+#                                                                                      +
+#  This program is free software: you can redistribute it and/or modify it under       +
+#  the terms of the GNU General Public License as published by the Free Software       +
+#  Foundation, either version 3 of the License, or (at your option) any later version. +
+#                                                                                      +
+#  This program is distributed in the hope that it will be useful, but WITHOUT ANY     +
+#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR       +
+#  A PARTICULAR PURPOSE. See the GNU General Public License for more details.          +
+#                                                                                      +
+#  You should have received a copy of the GNU General Public License along with        +
+#  this program.  If not, see <https://www.gnu.org/licenses/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+import pytest
+
+from vigenere_api.models.helpers.errors import (
+    BadKeyError,
+    EmptyKeyError,
+    KeyTypeError,
+    TooShortKeyError,
+)
+from vigenere_api.models.vigenere import VigenereKey
+
+
+class CtorSuite:
+    @staticmethod
+    def test_with_key() -> None:
+        key = "zz"
+        data = VigenereKey(key)
+
+        assert data._VigenereKey__key == key
+        assert len(data) == len(key)
+
+    @staticmethod
+    @pytest.mark.raises(exception=KeyTypeError)
+    def test_bad_type_key() -> None:
+        key = b"z"
+        _ignored_data = VigenereKey(key)
+
+    @staticmethod
+    @pytest.mark.raises(exception=EmptyKeyError)
+    def test_bad_empty_key() -> None:
+        key = ""
+        _ignored_data = VigenereKey(key)
+
+    @staticmethod
+    @pytest.mark.raises(exception=TooShortKeyError)
+    def test_too_short_key() -> None:
+        key = "e"
+        _ignored_data = VigenereKey(key)
+
+    @staticmethod
+    @pytest.mark.raises(exception=BadKeyError)
+    def test_bad_not_alpha_str_key() -> None:
+        key = "$z"
+        _ignored_data = VigenereKey(key)
+
+
+def test_next() -> None:
+    key = "abcd"
+    data = VigenereKey(key)
+
+    assert data._VigenereKey__key == key
+    assert len(data) == len(key)
+
+    for char in key:
+        assert next(data) == char
+
+    assert next(data) == key[0]
+
+
+def test_increase_index() -> None:
+    key = "abcd"
+    data = VigenereKey(key)
+
+    assert data._VigenereKey__key == key
+    assert len(data) == len(key)
+
+    assert data._VigenereKey__index == 0
+    data._VigenereKey__increase_index()
+    assert data._VigenereKey__index == 1
+
+    for _ in range(len(data)):
+        data._VigenereKey__increase_index()
+
+    assert data._VigenereKey__index == 1
-- 
GitLab