diff --git a/src/vigenere_api/api/app.py b/src/vigenere_api/api/app.py
index 7dfd7f287e9a471f0cf5dd3ae0ed4da2fd4b9b9c..90cfcd57194c1a072567c62132492780a288532d 100644
--- a/src/vigenere_api/api/app.py
+++ b/src/vigenere_api/api/app.py
@@ -20,10 +20,12 @@ from blacksheep import Application, Response
 from blacksheep.server.env import is_development
 from blacksheep.server.responses import redirect
 
-from vigenere_api.version import get_version
+from vigenere_api.version import get_version, Version
 
 from .v1.controllers import CaesarController as V1CaesarController
 from .v1.openapi_docs import docs as v1_docs
+from .v2.controllers import CaesarController as V2CaesarController, VigenereController
+from .v2.openapi_docs import docs as v2_docs
 
 
 application = Application()
@@ -40,12 +42,15 @@ if is_development():  # pragma: no cover
     application.debug = True
     application.show_error_details = True
 
-application.register_controllers([V1CaesarController])
+application.register_controllers(
+    [V1CaesarController, V2CaesarController, VigenereController],
+)
 v1_docs.bind_app(application)
+v2_docs.bind_app(application)
 
 get = application.router.get
 
-version = get_version()
+app_version: Version = get_version()
 
 
 @v1_docs(ignored=True)
@@ -61,7 +66,7 @@ async def index() -> Response:
     redirect
         Response
     """
-    return redirect(f"/api/v{version.major}")
+    return redirect(f"/api/v{app_version.major}")
 
 
 def __fallback() -> str:
diff --git a/src/vigenere_api/api/helpers/__init__.py b/src/vigenere_api/api/helpers/__init__.py
index 98e67f32ce5c5cdfe64923c0b2603abcf0f813ac..436c1b50686da026f0de9d45423151c8cc73a8bc 100644
--- a/src/vigenere_api/api/helpers/__init__.py
+++ b/src/vigenere_api/api/helpers/__init__.py
@@ -18,6 +18,13 @@
 
 from .controller import Controller
 from .open_api_handler import VigenereAPIOpenAPIHandler
+from .operation_docs import Algorithm, ControllerDocs, Operation
 
 
-__all__ = ["VigenereAPIOpenAPIHandler", "Controller"]
+__all__ = [
+    "VigenereAPIOpenAPIHandler",
+    "Controller",
+    "ControllerDocs",
+    "Operation",
+    "Algorithm",
+]
diff --git a/src/vigenere_api/api/helpers/errors.py b/src/vigenere_api/api/helpers/errors.py
index fd3c76a589a939d046ee1626bbdbbf6989b5df36..547fd2223cab8f998a1688a61fe892fa05936f3e 100644
--- a/src/vigenere_api/api/helpers/errors.py
+++ b/src/vigenere_api/api/helpers/errors.py
@@ -73,3 +73,44 @@ class PathTypeError(VigenereAPITypeError):
     def __init__(self, path: Any) -> None:
         """Create a new PathTypeError."""
         super().__init__(path, "path", "a string")
+
+
+@final
+class OperationTypeError(VigenereAPITypeError):
+    """Thrown if 'operation' is not an Operation object."""
+
+    def __init__(self, operation: Any) -> None:
+        """Create a new OperationTypeError."""
+        super().__init__(operation, "operation", "an Operation object")
+
+
+@final
+class AlgorithmTypeError(VigenereAPITypeError):
+    """Thrown if 'algorithm' is not an Algorithm object."""
+
+    def __init__(self, algorithm: Any) -> None:
+        """Create a new AlgorithmTypeError."""
+        super().__init__(algorithm, "algorithm", "an Algorithm object")
+
+
+@final
+class ExamplesTypeError(VigenereAPITypeError):
+    """Thrown if 'examples' is not a Sequence of CaesarData or VigenereData."""
+
+    def __init__(self, data: Any, name: str) -> None:
+        """Create a new ExamplesTypeError."""
+        super().__init__(data, name, "a Sequence of CaesarData or VigenereData")
+
+
+@final
+class ExampleTypeError(TypeError):
+    """Thrown if an example in 'data_examples' is not a CaesarData or VigenereData."""
+
+    def __init__(self, example: Any, name: str) -> None:
+        """Create a new ExampleTypeError."""
+        cls_name = type(example).__qualname__
+
+        super().__init__(
+            f"An example is a '{cls_name}' in {name}."
+            + " Please give a Sequence of CaesarData or VigenereData.",
+        )
diff --git a/src/vigenere_api/api/helpers/open_api_handler.py b/src/vigenere_api/api/helpers/open_api_handler.py
index b153e41225299730cdc3e597ce503cffd885ffb6..dc60aec2ec9131fd7a5e5f3ce5af7e3ce2ec3b2d 100644
--- a/src/vigenere_api/api/helpers/open_api_handler.py
+++ b/src/vigenere_api/api/helpers/open_api_handler.py
@@ -20,10 +20,11 @@ from typing import final, Final
 
 from blacksheep.server.openapi.ui import ReDocUIProvider
 from blacksheep.server.openapi.v3 import OpenAPIHandler
+
 from openapidocs.common import Format
 from openapidocs.v3 import Contact, ExternalDocs, Info, License, OpenAPI, Tag
-
 from vigenere_api.version import Version
+
 from .errors import VersionTypeError
 from .open_api_route_filter import get_route_filter
 
diff --git a/src/vigenere_api/api/helpers/open_api_route_filter.py b/src/vigenere_api/api/helpers/open_api_route_filter.py
index 4dd7c999212bbd10a99e8ceddbb35fc8e0a88839..45e2767b0c060b2186340df1345450ce84004e66 100644
--- a/src/vigenere_api/api/helpers/open_api_route_filter.py
+++ b/src/vigenere_api/api/helpers/open_api_route_filter.py
@@ -21,6 +21,7 @@ from collections.abc import Callable, Collection
 from blacksheep import Route
 
 from vigenere_api.version import Version
+
 from .errors import (
     ExcludedPathsTypeError,
     ExcludedPathTypeError,
diff --git a/src/vigenere_api/api/helpers/operation_docs.py b/src/vigenere_api/api/helpers/operation_docs.py
new file mode 100644
index 0000000000000000000000000000000000000000..ce69b3609d2652a6e6c8a202b432c708511de71d
--- /dev/null
+++ b/src/vigenere_api/api/helpers/operation_docs.py
@@ -0,0 +1,164 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+"""A controller's documentation."""
+
+from collections.abc import Sequence
+from dataclasses import dataclass
+from enum import auto, unique
+from http import HTTPStatus
+from typing import final, Union
+
+from blacksheep.server.openapi.common import (
+    ContentInfo,
+    EndpointDocs,
+    RequestBodyInfo,
+    ResponseExample,
+    ResponseInfo,
+)
+
+from strenum import LowercaseStrEnum, PascalCaseStrEnum
+from vigenere_api.models import CaesarData, VigenereData
+
+from .errors import (
+    AlgorithmTypeError,
+    ExamplesTypeError,
+    ExampleTypeError,
+    OperationTypeError,
+)
+
+
+@final
+@unique
+class Operation(LowercaseStrEnum):
+    """All Caesar or Vigenere operations."""
+
+    CIPHER = auto()
+    DECIPHER = auto()
+
+
+@final
+@unique
+class Algorithm(PascalCaseStrEnum):
+    """All algorithms available."""
+
+    CAESAR = auto()
+    VIGENERE = auto()
+
+
+@dataclass
+class ControllerDocs(EndpointDocs):
+    """Create the documentation for Caesar algorithm."""
+
+    def __init__(
+        self,
+        operation: Operation,
+        algorithm: Algorithm,
+        data1_examples: Sequence[Union[CaesarData, VigenereData]],
+        data2_examples: Sequence[Union[CaesarData, VigenereData]],
+    ) -> None:
+        """
+        Create a ControllerDocs.
+
+        Parameters
+        ----------
+        operation : Operation
+            The operation type.
+        algorithm : Algorithm
+            The algorithm type.
+        data1_examples : Sequence[Union[CaesarData, VigenereData]]
+            The first set of examples.
+        data2_examples : Sequence[Union[CaesarData, VigenereData]]
+            The second set of examples.
+
+        Raises
+        ------
+        OperationTypeError
+            Thrown if 'operation' is not an Operation object.
+        AlgorithmTypeError
+            Thrown if 'algorithm' is not an Algorithm object.
+        ExamplesTypeError
+            Thrown if 'data1_examples' is not a Sequence object.
+        ExamplesTypeError
+            Thrown if 'data2_examples' is not a Sequence object.
+        ExampleTypeError
+            Thrown if 'data1_examples[i]' is not a CaesarData or VigenereData object.
+        ExampleTypeError
+            Thrown if 'data2_examples[i]' is not a CaesarData or VigenereData object.
+        """
+        if not isinstance(operation, Operation):
+            raise OperationTypeError(operation)
+
+        if not isinstance(algorithm, Algorithm):
+            raise AlgorithmTypeError(algorithm)
+
+        if not isinstance(data1_examples, Sequence):
+            raise ExamplesTypeError(data1_examples, "data1_examples")
+
+        if not isinstance(data2_examples, Sequence):
+            raise ExamplesTypeError(data2_examples, "data2_examples")
+
+        for example in data1_examples:
+            if not isinstance(example, (CaesarData, VigenereData)):
+                raise ExampleTypeError(example, "data1_examples")
+
+        for example in data2_examples:
+            if not isinstance(example, (CaesarData, VigenereData)):
+                raise ExampleTypeError(example, "data2_examples")
+
+        response_examples = [ResponseExample(value=data) for data in data1_examples]
+        request_examples = {
+            f"example {i}": data for i, data in enumerate(data2_examples)
+        }
+
+        if operation == Operation.DECIPHER:
+            response_examples = [ResponseExample(value=data) for data in data2_examples]
+            request_examples = {
+                f"example {i}": data for i, data in enumerate(data1_examples)
+            }
+
+        response_type = VigenereData if algorithm == Algorithm.VIGENERE else CaesarData
+
+        ok_res = ResponseInfo(
+            description=f"Success {operation.value} with {algorithm} algorithm.",
+            content=[
+                ContentInfo(
+                    type=response_type,
+                    examples=response_examples,
+                ),
+            ],
+        )
+
+        summary_str = (
+            f"Apply the {algorithm} algorithm to {operation.value} the content."
+        )
+
+        super().__init__(
+            summary=summary_str,
+            description=(
+                f"Use the key with the {algorithm} algorithm"
+                + f" to {operation.value} the content."
+            ),
+            tags=[f"{algorithm}"],
+            request_body=RequestBodyInfo(
+                description="Examples of requests body.",
+                examples=request_examples,
+            ),
+            responses={
+                HTTPStatus.OK: ok_res,
+                HTTPStatus.BAD_REQUEST: "Bad request.",
+            },
+        )
diff --git a/src/vigenere_api/api/v1/controllers/caesar/docs.py b/src/vigenere_api/api/v1/controllers/caesar/docs.py
index a58153f93788587b221fd8adb7abda2cdfab7ca8..5b7ef9c40a8b4228ff6caa1d697b481711ada0a1 100644
--- a/src/vigenere_api/api/v1/controllers/caesar/docs.py
+++ b/src/vigenere_api/api/v1/controllers/caesar/docs.py
@@ -18,31 +18,12 @@
 """The caesar controller's documentation."""
 
 from dataclasses import dataclass
-from enum import auto, unique
-from http import HTTPStatus
 from typing import final
 
-from blacksheep.server.openapi.common import (
-    ContentInfo,
-    EndpointDocs,
-    RequestBodyInfo,
-    ResponseExample,
-    ResponseInfo,
-)
-
-from strenum import LowercaseStrEnum
+from vigenere_api.api.helpers.operation_docs import Algorithm, ControllerDocs, Operation
 from vigenere_api.models import CaesarData
 
 
-@final
-@unique
-class CaesarOperation(LowercaseStrEnum):
-    """All Caesar operations."""
-
-    CIPHER = auto()
-    DECIPHER = auto()
-
-
 CAESAR_DATA1 = (
     CaesarData(content="DeFgHiJkLmNoPqRsTuVwXyZaBc", key=3),
     CaesarData(content="DeFgHiJkLmNoPqRsTuVwXyZaBc", key="D"),
@@ -57,67 +38,19 @@ CAESAR_DATA2 = (
 
 @final
 @dataclass
-class CaesarControllerDocs(EndpointDocs):
+class CaesarControllerDocs(ControllerDocs):
     """Create the documentation for Caesar algorithm."""
 
-    def __init__(self, operation: CaesarOperation) -> None:
+    def __init__(self, operation: Operation) -> None:
         """
         Create a CaesarControllerDocs.
 
         Parameters
         ----------
-        operation : CaesarOperation
+        operation : Operation
         """
-        response_examples = [
-            ResponseExample(value=CAESAR_DATA1[0]),
-            ResponseExample(value=CAESAR_DATA1[1]),
-            ResponseExample(value=CAESAR_DATA1[2]),
-        ]
-        request_examples = {
-            "example 0": CAESAR_DATA2[0],
-            "example 1": CAESAR_DATA2[1],
-            "example 2": CAESAR_DATA2[2],
-        }
-
-        if operation == CaesarOperation.DECIPHER:
-            response_examples = [
-                ResponseExample(value=CAESAR_DATA2[0]),
-                ResponseExample(value=CAESAR_DATA2[1]),
-                ResponseExample(value=CAESAR_DATA2[2]),
-            ]
-            request_examples = {
-                "example 0": CAESAR_DATA1[0],
-                "example 1": CAESAR_DATA1[1],
-                "example 2": CAESAR_DATA1[2],
-            }
-
-        super().__init__(
-            summary=(
-                "Apply the Caesar algorithm to" + f" {operation.value} the content."
-            ),
-            description=(
-                "Use the key with the Caesar algorithm to"
-                + f" {operation.value} the content."
-            ),
-            tags=["Caesar"],
-            request_body=RequestBodyInfo(
-                description="Examples of requests body.",
-                examples=request_examples,
-            ),
-            responses={
-                HTTPStatus.OK: ResponseInfo(
-                    description=f"Success {operation.value} with Caesar algorithm.",
-                    content=[
-                        ContentInfo(
-                            type=CaesarData,
-                            examples=response_examples,
-                        ),
-                    ],
-                ),
-                HTTPStatus.BAD_REQUEST: "Bad request.",
-            },
-        )
+        super().__init__(operation, Algorithm.CAESAR, CAESAR_DATA1, CAESAR_DATA2)
 
 
-post_caesar_cipher_docs = CaesarControllerDocs(operation=CaesarOperation.CIPHER)
-post_caesar_decipher_docs = CaesarControllerDocs(operation=CaesarOperation.DECIPHER)
+post_caesar_cipher_docs = CaesarControllerDocs(operation=Operation.CIPHER)
+post_caesar_decipher_docs = CaesarControllerDocs(operation=Operation.DECIPHER)
diff --git a/src/vigenere_api/api/v2/__init__.py b/src/vigenere_api/api/v2/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3cc3eafea10f8d6e434762b63b8962560c9b6281
--- /dev/null
+++ b/src/vigenere_api/api/v2/__init__.py
@@ -0,0 +1,17 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+"""Package of the second API version."""
diff --git a/src/vigenere_api/api/v2/controllers/__init__.py b/src/vigenere_api/api/v2/controllers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..10a95044e8d49c160bab556e31968037c5e709b1
--- /dev/null
+++ b/src/vigenere_api/api/v2/controllers/__init__.py
@@ -0,0 +1,23 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+"""Controllers in the V2 API."""
+
+from .caesar import CaesarController
+from .vigenere import VigenereController
+
+
+__all__ = ["CaesarController", "VigenereController"]
diff --git a/src/vigenere_api/api/v2/controllers/caesar/__init__.py b/src/vigenere_api/api/v2/controllers/caesar/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4fb5e9363898f91827b608280dec0816465c3997
--- /dev/null
+++ b/src/vigenere_api/api/v2/controllers/caesar/__init__.py
@@ -0,0 +1,22 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+"""CaesarController package."""
+
+from .caesar import CaesarController
+
+
+__all__ = ["CaesarController"]
diff --git a/src/vigenere_api/api/v2/controllers/caesar/caesar.py b/src/vigenere_api/api/v2/controllers/caesar/caesar.py
new file mode 100644
index 0000000000000000000000000000000000000000..fc73027a502dceb09004e6cd600fd54ba32e28d9
--- /dev/null
+++ b/src/vigenere_api/api/v2/controllers/caesar/caesar.py
@@ -0,0 +1,91 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+"""The caesar controller."""
+from typing import final
+
+from blacksheep import FromJSON, Response
+from blacksheep.server.controllers import post
+
+from vigenere_api.api.helpers import Controller
+from vigenere_api.api.v1.controllers import CaesarController as V1CaesarController
+from vigenere_api.api.v1.controllers.caesar.docs import (
+    post_caesar_cipher_docs,
+    post_caesar_decipher_docs,
+)
+from vigenere_api.api.v2.openapi_docs import docs
+from vigenere_api.models import CaesarData
+
+
+@final
+class CaesarController(Controller):
+    """
+    The caesar controller.
+
+    This controller calls functions of V1 controller.
+
+    Provides routes:
+    - POST /api/v2/caesar/cipher
+    - POST /api/v2/caesar/decipher
+    """
+
+    @classmethod
+    def version(cls) -> str:
+        """
+        Version of the API.
+
+        Returns
+        -------
+        version
+            str
+        """
+        return f"v{docs.version.major}"
+
+    @docs(post_caesar_cipher_docs)
+    @post("cipher")
+    async def cipher(self, data: FromJSON[CaesarData]) -> Response:
+        """
+        Cipher the input request with Caesar algorithm.
+
+        Parameters
+        ----------
+        data : CaesarData
+            A CaesarData from JSON from the request body.
+
+        Returns
+        -------
+        response
+            Response
+        """
+        return await V1CaesarController.cipher(self, data)
+
+    @docs(post_caesar_decipher_docs)
+    @post("decipher")
+    async def decipher(self, data: FromJSON[CaesarData]) -> Response:
+        """
+        Decipher the input request with Caesar algorithm.
+
+        Parameters
+        ----------
+        data : CaesarData
+            A CaesarData from JSON from the request body.
+
+        Returns
+        -------
+        response
+            Response
+        """
+        return await V1CaesarController.decipher(self, data)
diff --git a/src/vigenere_api/api/v2/controllers/caesar/caesar.pyi b/src/vigenere_api/api/v2/controllers/caesar/caesar.pyi
new file mode 100644
index 0000000000000000000000000000000000000000..868c77b89d30555ed72638847fa45cd4e5c54161
--- /dev/null
+++ b/src/vigenere_api/api/v2/controllers/caesar/caesar.pyi
@@ -0,0 +1,24 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+from blacksheep import FromJSON, Response
+from blacksheep.server.controllers import APIController
+
+from vigenere_api.models import CaesarData
+
+class CaesarController(APIController):
+    async def cipher(self, data: FromJSON[CaesarData]) -> Response: ...
+    async def decipher(self, data: FromJSON[CaesarData]) -> Response: ...
diff --git a/src/vigenere_api/api/v2/controllers/vigenere/__init__.py b/src/vigenere_api/api/v2/controllers/vigenere/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ee7f9e9f95452cffaf4470b5caa410397965810
--- /dev/null
+++ b/src/vigenere_api/api/v2/controllers/vigenere/__init__.py
@@ -0,0 +1,22 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+"""CaesarController package."""
+
+from .vigenere import VigenereController
+
+
+__all__ = ["VigenereController"]
diff --git a/src/vigenere_api/api/v2/controllers/vigenere/docs.py b/src/vigenere_api/api/v2/controllers/vigenere/docs.py
new file mode 100644
index 0000000000000000000000000000000000000000..b78481c08a29652dadca7038bab845f5b4366f7d
--- /dev/null
+++ b/src/vigenere_api/api/v2/controllers/vigenere/docs.py
@@ -0,0 +1,58 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+
+"""The caesar controller's documentation."""
+
+from dataclasses import dataclass
+from typing import final
+
+from vigenere_api.api.helpers import Algorithm, ControllerDocs, Operation
+from vigenere_api.models import VigenereData
+
+
+VIGENERE_DATA1 = (
+    VigenereData(content="RI ZR VXGM XFLX CWMI", key="pierre"),
+    VigenereData(content="ABC DAB CDA BCD ABC DAB", key="AbCd"),
+    VigenereData(content="Ri zr vxgm xflx cwmi!", key="PIERRE"),
+    VigenereData(content="AbC dab CDA bCd abc dAB", key="abcd"),
+)
+VIGENERE_DATA2 = (
+    VigenereData(content="CA VA ETRE TOUT NOIR", key="PIERRE"),
+    VigenereData(content="AAA AAA AAA AAA AAA AAA", key="abcd"),
+    VigenereData(content="Ca va etre tout noir!", key="piErrE"),
+    VigenereData(content="AaA aaa AAA aAa aaa aAA", key="aBCd"),
+)
+
+
+@final
+@dataclass
+class VigenereControllerDocs(ControllerDocs):
+    """Create the documentation for Vigenere algorithm."""
+
+    def __init__(self, operation: Operation) -> None:
+        """
+        Create a VigenereControllerDocs.
+
+        Parameters
+        ----------
+        operation : Operation
+        """
+        super().__init__(operation, Algorithm.VIGENERE, VIGENERE_DATA1, VIGENERE_DATA2)
+
+
+post_vigenere_cipher_docs = VigenereControllerDocs(operation=Operation.CIPHER)
+post_vigenere_decipher_docs = VigenereControllerDocs(operation=Operation.DECIPHER)
diff --git a/src/vigenere_api/api/v2/controllers/vigenere/vigenere.py b/src/vigenere_api/api/v2/controllers/vigenere/vigenere.py
new file mode 100644
index 0000000000000000000000000000000000000000..2ce74f7a9de7a302f9c6a0d8e72bd2016286f870
--- /dev/null
+++ b/src/vigenere_api/api/v2/controllers/vigenere/vigenere.py
@@ -0,0 +1,87 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+"""The caesar controller."""
+
+from typing import final
+
+from blacksheep import FromJSON, Response
+from blacksheep.server.controllers import post
+
+from vigenere_api.api.helpers import Controller
+from vigenere_api.api.v2.openapi_docs import docs
+from vigenere_api.models import VigenereData
+
+from .docs import post_vigenere_cipher_docs, post_vigenere_decipher_docs
+
+
+@final
+class VigenereController(Controller):
+    """
+    The vigenere controller.
+
+    Provides routes:
+    - POST /api/v2/vigenere/cipher
+    - POST /api/v2/vigenere/decipher
+    """
+
+    @classmethod
+    def version(cls) -> str:
+        """
+        Version of the API.
+
+        Returns
+        -------
+        version
+            str
+        """
+        return f"v{docs.version.major}"
+
+    @docs(post_vigenere_cipher_docs)
+    @post("cipher")
+    async def cipher(self, data: FromJSON[VigenereData]) -> Response:
+        """
+        Cipher the input request with Vigenere algorithm.
+
+        Parameters
+        ----------
+        data : VigenereData
+            A VigenereData from JSON from the request body.
+
+        Returns
+        -------
+        response
+            Response
+        """
+        return self.json(data.value.cipher())
+
+    @docs(post_vigenere_decipher_docs)
+    @post("decipher")
+    async def decipher(self, data: FromJSON[VigenereData]) -> Response:
+        """
+        Decipher the input request with Vigenere algorithm.
+
+        Parameters
+        ----------
+        data : VigenereData
+            A VigenereData from JSON from the request body.
+
+        Returns
+        -------
+        response
+            Response
+        """
+        return self.json(data.value.decipher())
diff --git a/src/vigenere_api/api/v2/controllers/vigenere/vigenere.pyi b/src/vigenere_api/api/v2/controllers/vigenere/vigenere.pyi
new file mode 100644
index 0000000000000000000000000000000000000000..149794e0db90563f1c146522a230b41281f2fb30
--- /dev/null
+++ b/src/vigenere_api/api/v2/controllers/vigenere/vigenere.pyi
@@ -0,0 +1,24 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+from blacksheep import FromJSON, Response
+from blacksheep.server.controllers import APIController
+
+from vigenere_api.models import VigenereData
+
+class VigenereController(APIController):
+    async def cipher(self, data: FromJSON[VigenereData]) -> Response: ...
+    async def decipher(self, data: FromJSON[VigenereData]) -> Response: ...
diff --git a/src/vigenere_api/api/v2/openapi_docs.py b/src/vigenere_api/api/v2/openapi_docs.py
new file mode 100644
index 0000000000000000000000000000000000000000..9551fe909089381bf25b9363bf3e9be94db0fb2e
--- /dev/null
+++ b/src/vigenere_api/api/v2/openapi_docs.py
@@ -0,0 +1,23 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+"""Common OpenAPI docs."""
+
+from vigenere_api.api.helpers import VigenereAPIOpenAPIHandler
+from vigenere_api.version import Version
+
+
+docs = VigenereAPIOpenAPIHandler(Version(major=2, minor=0, patch=0))
diff --git a/src/vigenere_api/models/base_data.py b/src/vigenere_api/models/base_data.py
new file mode 100644
index 0000000000000000000000000000000000000000..6d5c5148c9fdc711e232d0b5734c61c2316b1191
--- /dev/null
+++ b/src/vigenere_api/models/base_data.py
@@ -0,0 +1,62 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+"""Base model."""
+
+from __future__ import annotations
+
+from pydantic import StrictStr, validator
+
+from vigenere_api.helpers import Model
+
+from .errors import ContentTypeError, EmptyContentError
+
+
+class BaseData(Model):
+    """Base data to verify the content."""
+
+    content: StrictStr
+    """The content to be ciphered or deciphered."""
+
+    @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
diff --git a/src/vigenere_api/models/caesar.py b/src/vigenere_api/models/caesar.py
index 82cb12ab78f258c88be49352c48dbd583bb7f684..6ec4c9bb03e77f002a6d805812ad53b0389afbc8 100644
--- a/src/vigenere_api/models/caesar.py
+++ b/src/vigenere_api/models/caesar.py
@@ -22,13 +22,11 @@ from typing import final, Union
 
 from pydantic import StrictInt, StrictStr, validator
 
-from vigenere_api.helpers import Model
+from .base_data import BaseData
 from .errors import (
     AlgorithmExpectedKeyType,
     AlgorithmKeyTypeError,
     AlgorithmTextTypeError,
-    ContentTypeError,
-    EmptyContentError,
 )
 from .helpers import convert_key, move_char
 from .helpers.errors import (
@@ -39,11 +37,12 @@ from .helpers.errors import (
     TooLongKeyError,
 )
 
+
 Key = Union[StrictInt, StrictStr]
 
 
 @final
-class CaesarData(Model):
+class CaesarData(BaseData):
     """
     Caesar data to cipher the content or decipher.
 
@@ -62,9 +61,6 @@ class CaesarData(Model):
     >>> assert caesar_data.key == ciphered_data.key == deciphered_data.key == 1
     """
 
-    content: StrictStr
-    """The content to be ciphered or deciphered."""
-
     key: Key
     """The key to cipher or decipher the content."""
 
@@ -153,36 +149,6 @@ class CaesarData(Model):
 
         return convert_key(self.key)
 
-    @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: Key) -> Key:
         """
diff --git a/src/vigenere_api/models/errors.py b/src/vigenere_api/models/errors.py
index 76cf57bc84748840a71e770145fc73b130bb42af..5d1670b5ba867cb55ac8f11d303abd74c349cf27 100644
--- a/src/vigenere_api/models/errors.py
+++ b/src/vigenere_api/models/errors.py
@@ -105,4 +105,11 @@ class AlgorithmOperationTypeError(VigenereAPITypeError):
     """Thrown if the operation is not a VigenereOperation object."""
 
     def __init__(self, operation: Any) -> None:
+        """
+        Create an AlgorithmOperationTypeError with the operation.
+
+        Parameters
+        ----------
+        operation : Any
+        """
         super().__init__(operation, "operation", "a VigenereOperation object")
diff --git a/src/vigenere_api/models/helpers/__init__.py b/src/vigenere_api/models/helpers/__init__.py
index 35481ef243f15882abd43f3db48f6e6ccd477e7c..5e1f489fe893f837c9264b91b37191c9323d6801 100644
--- a/src/vigenere_api/models/helpers/__init__.py
+++ b/src/vigenere_api/models/helpers/__init__.py
@@ -16,7 +16,9 @@
 
 """Helper package for models."""
 
-from .helper import convert_key, move_char
+from .convert_key import convert_key
+from .move_char import move_char
 from .vigenere_key import VigenereKey
 
+
 __all__ = ["move_char", "convert_key", "VigenereKey"]
diff --git a/src/vigenere_api/models/helpers/check_key.py b/src/vigenere_api/models/helpers/check_key.py
new file mode 100644
index 0000000000000000000000000000000000000000..f89ec326f948ebfbfdc088300ee0137b7d30c508
--- /dev/null
+++ b/src/vigenere_api/models/helpers/check_key.py
@@ -0,0 +1,47 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+"""Check if the key is good."""
+
+from .errors import BadKeyError, EmptyKeyError, ExpectedKeyType, KeyTypeError
+
+
+def check_key(key: str) -> None:
+    """
+    Check if the key is an alphabetic string and not empty.
+
+    Parameters
+    ----------
+    key : str
+        The key to check.
+
+    Raises
+    ------
+    KeyTypeError
+        Thrown if 'key' is not a string.
+    EmptyKeyError
+        Thrown if 'key' is empty.
+    BadKeyError
+        Thrown if 'key' is not an alphabetic string.
+    """
+    if not isinstance(key, str):
+        raise KeyTypeError(key, ExpectedKeyType.STRING)
+
+    if len(key) == 0:
+        raise EmptyKeyError
+
+    if not key.isalpha():
+        raise BadKeyError(key, ExpectedKeyType.STRING)
diff --git a/src/vigenere_api/models/helpers/convert_key.py b/src/vigenere_api/models/helpers/convert_key.py
new file mode 100644
index 0000000000000000000000000000000000000000..c4b558cfa4caace4153df070a131c081d7bbc623
--- /dev/null
+++ b/src/vigenere_api/models/helpers/convert_key.py
@@ -0,0 +1,53 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+"""Convert a one character string into an integer between 0 and 25."""
+
+from .check_key import check_key
+from .errors import TooLongKeyError
+
+
+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
+    """
+    check_key(key)
+
+    if len(key) > 1:
+        raise TooLongKeyError
+
+    return ord(key) - ord("A") if key.isupper() else ord(key) - ord("a")
diff --git a/src/vigenere_api/models/helpers/errors.py b/src/vigenere_api/models/helpers/errors.py
index 569950e38b8ef5941b02e426116cba1c9b908f6f..14c1d9f36fc8f2d8006c33a31c7e50718d38902f 100644
--- a/src/vigenere_api/models/helpers/errors.py
+++ b/src/vigenere_api/models/helpers/errors.py
@@ -15,12 +15,14 @@
 # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 
 """All errors thrown by the helper."""
+
 from enum import unique
 from typing import Any, final
 
 from strenum import StrEnum
 from vigenere_api.helpers import VigenereAPITypeError
 
+
 A_STRING = "a string"
 
 
diff --git a/src/vigenere_api/models/helpers/helper.py b/src/vigenere_api/models/helpers/move_char.py
similarity index 76%
rename from src/vigenere_api/models/helpers/helper.py
rename to src/vigenere_api/models/helpers/move_char.py
index a91e933b76827246da61901f372c50803c434b93..0a5c41f3acd4f40588d323a6eccccdf3956fe145 100644
--- a/src/vigenere_api/models/helpers/helper.py
+++ b/src/vigenere_api/models/helpers/move_char.py
@@ -19,17 +19,12 @@
 from typing import Literal
 
 from .errors import (
-    BadKeyError,
-    EmptyKeyError,
-    ExpectedKeyType,
     HelperBadCharValueError,
     HelperBadFirstLetterValueError,
     HelperBadLengthCharValueError,
     HelperCharTypeError,
     HelperFirstLetterTypeError,
     HelperKeyTypeError,
-    KeyTypeError,
-    TooLongKeyError,
 )
 
 
@@ -85,43 +80,3 @@ 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
index db5f46b61c6812f89077c7447c89e273f36d5a45..118606da462aba3c8f49d22ce2fa1f6aedd4ce40 100644
--- a/src/vigenere_api/models/helpers/vigenere_key.py
+++ b/src/vigenere_api/models/helpers/vigenere_key.py
@@ -18,13 +18,8 @@
 
 from typing import final, Final
 
-from .errors import (
-    BadKeyError,
-    EmptyKeyError,
-    ExpectedKeyType,
-    KeyTypeError,
-    TooShortKeyError,
-)
+from .check_key import check_key
+from .errors import TooShortKeyError
 
 
 @final
@@ -51,19 +46,11 @@ class VigenereKey:
         BadKeyError
             Thrown if 'key' contains invalid characters.
         """
-
-        if not isinstance(key, str):
-            raise KeyTypeError(key, ExpectedKeyType.STRING)
-
-        if len(key) == 0:
-            raise EmptyKeyError
+        check_key(key)
 
         if len(key) == 1:
             raise TooShortKeyError
 
-        if not key.isalpha():
-            raise BadKeyError(key, ExpectedKeyType.STRING)
-
         self.__index = 0
         self.__key: Final = key
 
diff --git a/src/vigenere_api/models/vigenere.py b/src/vigenere_api/models/vigenere.py
index 17ca2aa0376598da695396b0c95ad3bf4f9f3918..a31ca4e9b8526ce48a7580979e7bb046dc4b6c06 100644
--- a/src/vigenere_api/models/vigenere.py
+++ b/src/vigenere_api/models/vigenere.py
@@ -24,14 +24,13 @@ from typing import final
 from pydantic import StrictStr, validator
 
 from strenum import StrEnum
-from vigenere_api.helpers import Model
+
+from .base_data import BaseData
 from .errors import (
     AlgorithmExpectedKeyType,
     AlgorithmKeyTypeError,
     AlgorithmOperationTypeError,
     AlgorithmTextTypeError,
-    ContentTypeError,
-    EmptyContentError,
 )
 from .helpers import convert_key, move_char, VigenereKey
 
@@ -46,7 +45,7 @@ class VigenereOperation(StrEnum):
 
 
 @final
-class VigenereData(Model):
+class VigenereData(BaseData):
     """
     Vigenere data to cipher the content or decipher.
 
@@ -65,9 +64,6 @@ class VigenereData(Model):
     >>> 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."""
 
@@ -160,36 +156,6 @@ class VigenereData(Model):
 
         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:
         """
diff --git a/src/vigenere_api/version/version.py b/src/vigenere_api/version/version.py
index 775fa33b73cd5c56f38c4dd1312fa1a22c5c2b94..8f100df586af580fcfbf7d4f055a8e2da6e36872 100644
--- a/src/vigenere_api/version/version.py
+++ b/src/vigenere_api/version/version.py
@@ -154,4 +154,4 @@ def get_version() -> Version:
     version
         Version
     """
-    return Version(major=1, minor=0, patch=0)
+    return Version(major=2, minor=0, patch=0)
diff --git a/tests/api/helpers/test_operation_docs.py b/tests/api/helpers/test_operation_docs.py
new file mode 100644
index 0000000000000000000000000000000000000000..0900718745d04bb345005349ffd36fe7eb254550
--- /dev/null
+++ b/tests/api/helpers/test_operation_docs.py
@@ -0,0 +1,246 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+from http import HTTPStatus
+
+import pytest
+from blacksheep.server.openapi.common import (
+    ContentInfo,
+    RequestBodyInfo,
+    ResponseExample,
+    ResponseInfo,
+)
+
+from vigenere_api.api.helpers import Algorithm, ControllerDocs, Operation
+from vigenere_api.api.helpers.errors import (
+    AlgorithmTypeError,
+    ExamplesTypeError,
+    ExampleTypeError,
+    OperationTypeError,
+)
+from vigenere_api.api.v1.controllers.caesar.docs import CAESAR_DATA1, CAESAR_DATA2
+from vigenere_api.api.v2.controllers.vigenere.docs import VIGENERE_DATA1, VIGENERE_DATA2
+from vigenere_api.models import CaesarData, VigenereData
+
+
+def test_operation_cipher_caesar() -> None:
+    docs = ControllerDocs(
+        Operation.CIPHER,
+        Algorithm.CAESAR,
+        CAESAR_DATA1,
+        CAESAR_DATA2,
+    )
+
+    assert docs.summary == "Apply the Caesar algorithm to cipher the content."
+    assert (
+        docs.description
+        == "Use the key with the Caesar algorithm to cipher the content."
+    )
+    assert "Caesar" in docs.tags
+    assert docs.request_body == RequestBodyInfo(
+        description="Examples of requests body.",
+        examples={
+            "example 0": CAESAR_DATA2[0],
+            "example 1": CAESAR_DATA2[1],
+            "example 2": CAESAR_DATA2[2],
+        },
+    )
+    assert docs.responses == {
+        HTTPStatus.OK: ResponseInfo(
+            description="Success cipher with Caesar algorithm.",
+            content=[
+                ContentInfo(
+                    type=CaesarData,
+                    examples=[
+                        ResponseExample(value=CAESAR_DATA1[0]),
+                        ResponseExample(value=CAESAR_DATA1[1]),
+                        ResponseExample(value=CAESAR_DATA1[2]),
+                    ],
+                ),
+            ],
+        ),
+        HTTPStatus.BAD_REQUEST: "Bad request.",
+    }
+
+
+def test_operation_decipher_caesar() -> None:
+    docs = ControllerDocs(
+        Operation.DECIPHER,
+        Algorithm.CAESAR,
+        CAESAR_DATA1,
+        CAESAR_DATA2,
+    )
+
+    assert docs.summary == "Apply the Caesar algorithm to decipher the content."
+    assert (
+        docs.description
+        == "Use the key with the Caesar algorithm to decipher the content."
+    )
+    assert "Caesar" in docs.tags
+    assert docs.request_body == RequestBodyInfo(
+        description="Examples of requests body.",
+        examples={
+            "example 0": CAESAR_DATA1[0],
+            "example 1": CAESAR_DATA1[1],
+            "example 2": CAESAR_DATA1[2],
+        },
+    )
+    assert docs.responses == {
+        HTTPStatus.OK: ResponseInfo(
+            description="Success decipher with Caesar algorithm.",
+            content=[
+                ContentInfo(
+                    type=CaesarData,
+                    examples=[
+                        ResponseExample(value=CAESAR_DATA2[0]),
+                        ResponseExample(value=CAESAR_DATA2[1]),
+                        ResponseExample(value=CAESAR_DATA2[2]),
+                    ],
+                ),
+            ],
+        ),
+        HTTPStatus.BAD_REQUEST: "Bad request.",
+    }
+
+
+def test_operation_cipher_vigenere() -> None:
+    docs = ControllerDocs(
+        Operation.CIPHER,
+        Algorithm.VIGENERE,
+        VIGENERE_DATA1,
+        VIGENERE_DATA2,
+    )
+
+    assert docs.summary == "Apply the Vigenere algorithm to cipher the content."
+    assert (
+        docs.description
+        == "Use the key with the Vigenere algorithm to cipher the content."
+    )
+    assert "Vigenere" in docs.tags
+    assert docs.request_body == RequestBodyInfo(
+        description="Examples of requests body.",
+        examples={
+            "example 0": VIGENERE_DATA2[0],
+            "example 1": VIGENERE_DATA2[1],
+            "example 2": VIGENERE_DATA2[2],
+            "example 3": VIGENERE_DATA2[3],
+        },
+    )
+    assert docs.responses == {
+        HTTPStatus.OK: ResponseInfo(
+            description="Success cipher with Vigenere algorithm.",
+            content=[
+                ContentInfo(
+                    type=VigenereData,
+                    examples=[
+                        ResponseExample(value=VIGENERE_DATA1[0]),
+                        ResponseExample(value=VIGENERE_DATA1[1]),
+                        ResponseExample(value=VIGENERE_DATA1[2]),
+                        ResponseExample(value=VIGENERE_DATA1[3]),
+                    ],
+                ),
+            ],
+        ),
+        HTTPStatus.BAD_REQUEST: "Bad request.",
+    }
+
+
+def test_operation_decipher_vigenere() -> None:
+    docs = ControllerDocs(
+        Operation.DECIPHER,
+        Algorithm.VIGENERE,
+        VIGENERE_DATA1,
+        VIGENERE_DATA2,
+    )
+
+    assert docs.summary == "Apply the Vigenere algorithm to decipher the content."
+    assert (
+        docs.description
+        == "Use the key with the Vigenere algorithm to decipher the content."
+    )
+    assert "Vigenere" in docs.tags
+    assert docs.request_body == RequestBodyInfo(
+        description="Examples of requests body.",
+        examples={
+            "example 0": VIGENERE_DATA1[0],
+            "example 1": VIGENERE_DATA1[1],
+            "example 2": VIGENERE_DATA1[2],
+            "example 3": VIGENERE_DATA1[3],
+        },
+    )
+    assert docs.responses == {
+        HTTPStatus.OK: ResponseInfo(
+            description="Success decipher with Vigenere algorithm.",
+            content=[
+                ContentInfo(
+                    type=VigenereData,
+                    examples=[
+                        ResponseExample(value=VIGENERE_DATA2[0]),
+                        ResponseExample(value=VIGENERE_DATA2[1]),
+                        ResponseExample(value=VIGENERE_DATA2[2]),
+                        ResponseExample(value=VIGENERE_DATA2[3]),
+                    ],
+                ),
+            ],
+        ),
+        HTTPStatus.BAD_REQUEST: "Bad request.",
+    }
+
+
+class BadTypeSuite:
+    @staticmethod
+    @pytest.mark.raises(exception=OperationTypeError)
+    def test_bad_operation() -> None:
+        ControllerDocs("Operation.CIPHER", Algorithm.CAESAR, CAESAR_DATA1, CAESAR_DATA2)
+
+    @staticmethod
+    @pytest.mark.raises(exception=AlgorithmTypeError)
+    def test_bad_algorithm() -> None:
+        ControllerDocs(Operation.CIPHER, "Algorithm.CAESAR", CAESAR_DATA1, CAESAR_DATA2)
+
+    @staticmethod
+    @pytest.mark.raises(exception=ExamplesTypeError, message="data1_examples")
+    def test_bad_sequence_data1() -> None:
+        ControllerDocs(
+            Operation.CIPHER, Algorithm.CAESAR, {"CAESAR_DATA1": "tere"}, CAESAR_DATA2
+        )
+
+    @staticmethod
+    @pytest.mark.raises(exception=ExamplesTypeError, message="data2_examples")
+    def test_bad_sequence_data2() -> None:
+        ControllerDocs(
+            Operation.CIPHER, Algorithm.CAESAR, CAESAR_DATA1, {"CAESAR_DATA1": "tere"}
+        )
+
+    @staticmethod
+    @pytest.mark.raises(exception=ExampleTypeError, message="data1_examples")
+    def test_bad_example_data1() -> None:
+        ControllerDocs(
+            Operation.CIPHER,
+            Algorithm.CAESAR,
+            ("ter", "ettr"),
+            CAESAR_DATA2,
+        )
+
+    @staticmethod
+    @pytest.mark.raises(exception=ExampleTypeError, message="data2_examples")
+    def test_bad_example_data2() -> None:
+        ControllerDocs(
+            Operation.CIPHER,
+            Algorithm.CAESAR,
+            CAESAR_DATA1,
+            ("ter", "ettr"),
+        )
diff --git a/tests/api/test_index.py b/tests/api/test_index.py
index 74f4522cbd7fe5a1921724e455a46dc9b723a0f7..7f3c9ec7ae0f9f5787601b9a1f9d5dc6d592e693 100644
--- a/tests/api/test_index.py
+++ b/tests/api/test_index.py
@@ -30,7 +30,7 @@ async def test_get_index(test_client: TestClient) -> None:
 
     first_header = response.headers.values[0]
     assert first_header[0] == b"Location"
-    assert first_header[1] == b"/api/v1"
+    assert first_header[1] == b"/api/v2"
 
 
 @pytest.mark.asyncio()
diff --git a/tests/api/v1/docs/test_caesar.py b/tests/api/v1/docs/test_caesar.py
index da998664ffbffc7c74b9765501c7a6f23cc8ff1d..34c8a882222c0c9945b106f179ff0f4a402e1dbf 100644
--- a/tests/api/v1/docs/test_caesar.py
+++ b/tests/api/v1/docs/test_caesar.py
@@ -16,6 +16,7 @@
 
 from http import HTTPStatus
 
+import pytest
 from blacksheep.server.openapi.common import (
     ContentInfo,
     RequestBodyInfo,
@@ -23,17 +24,18 @@ from blacksheep.server.openapi.common import (
     ResponseInfo,
 )
 
+from vigenere_api.api.helpers import Operation
+from vigenere_api.api.helpers.errors import OperationTypeError
 from vigenere_api.api.v1.controllers.caesar.docs import (
     CAESAR_DATA1,
     CAESAR_DATA2,
     CaesarControllerDocs,
-    CaesarOperation,
 )
 from vigenere_api.models import CaesarData
 
 
 def test_operation_cipher() -> None:
-    docs = CaesarControllerDocs(CaesarOperation.CIPHER)
+    docs = CaesarControllerDocs(Operation.CIPHER)
 
     assert docs.summary == "Apply the Caesar algorithm to cipher the content."
     assert (
@@ -68,7 +70,7 @@ def test_operation_cipher() -> None:
 
 
 def test_operation_decipher() -> None:
-    docs = CaesarControllerDocs(CaesarOperation.DECIPHER)
+    docs = CaesarControllerDocs(Operation.DECIPHER)
 
     assert docs.summary == "Apply the Caesar algorithm to decipher the content."
     assert (
@@ -100,3 +102,8 @@ def test_operation_decipher() -> None:
         ),
         HTTPStatus.BAD_REQUEST: "Bad request.",
     }
+
+
+@pytest.mark.raises(exception=OperationTypeError)
+def test_bad_type_operation() -> None:
+    CaesarControllerDocs("btest")
diff --git a/tests/api/v1/test_caesar.py b/tests/api/v1/test_caesar.py
index adb97b7de8b9b98252b474646d1169eb0c876aec..90818acd8ee0c229833776bba855f36f51d3676f 100644
--- a/tests/api/v1/test_caesar.py
+++ b/tests/api/v1/test_caesar.py
@@ -356,8 +356,7 @@ class CipherSuite:
             assert data == [
                 {
                     "loc": ["key"],
-                    "msg": "The key is too long. Please give a one character string or an "
-                    "integer.",
+                    "msg": "The key is too long. Please give a one character string or an integer.",
                     "type": "value_error.toolongkey",
                 },
             ]
@@ -378,8 +377,7 @@ class CipherSuite:
             assert data == [
                 {
                     "loc": ["key"],
-                    "msg": "The key '+' is invalid. Please give an alphabetic one character "
-                    "string or an integer.",
+                    "msg": "The key '+' is invalid. Please give a string or an integer.",
                     "type": "value_error.badkey",
                 },
             ]
@@ -718,8 +716,7 @@ class DecipherSuite:
             assert data == [
                 {
                     "loc": ["key"],
-                    "msg": "The key '+' is invalid. Please give an alphabetic one character "
-                    "string or an integer.",
+                    "msg": "The key '+' is invalid. Please give a string or an integer.",
                     "type": "value_error.badkey",
                 },
             ]
diff --git a/tests/api/v2/__init__.py b/tests/api/v2/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..bf8de0773dc8411127d6bbaa8262a9194c6fc4c0
--- /dev/null
+++ b/tests/api/v2/__init__.py
@@ -0,0 +1,15 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/tests/api/v2/docs/__init__.py b/tests/api/v2/docs/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..bf8de0773dc8411127d6bbaa8262a9194c6fc4c0
--- /dev/null
+++ b/tests/api/v2/docs/__init__.py
@@ -0,0 +1,15 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/tests/api/v2/docs/test_vigenere.py b/tests/api/v2/docs/test_vigenere.py
new file mode 100644
index 0000000000000000000000000000000000000000..367a98a09ac1cc01ac24bac2df512d5ea917a55e
--- /dev/null
+++ b/tests/api/v2/docs/test_vigenere.py
@@ -0,0 +1,113 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+from http import HTTPStatus
+
+import pytest
+from blacksheep.server.openapi.common import (
+    ContentInfo,
+    RequestBodyInfo,
+    ResponseExample,
+    ResponseInfo,
+)
+
+from vigenere_api.api.helpers import Operation
+from vigenere_api.api.helpers.errors import OperationTypeError
+from vigenere_api.api.v2.controllers.vigenere.docs import (
+    VIGENERE_DATA1,
+    VIGENERE_DATA2,
+    VigenereControllerDocs,
+)
+from vigenere_api.models import VigenereData
+
+
+def test_operation_cipher() -> None:
+    docs = VigenereControllerDocs(Operation.CIPHER)
+
+    assert docs.summary == "Apply the Vigenere algorithm to cipher the content."
+    assert (
+        docs.description
+        == "Use the key with the Vigenere algorithm to cipher the content."
+    )
+    assert "Vigenere" in docs.tags
+    assert docs.request_body == RequestBodyInfo(
+        description="Examples of requests body.",
+        examples={
+            "example 0": VIGENERE_DATA2[0],
+            "example 1": VIGENERE_DATA2[1],
+            "example 2": VIGENERE_DATA2[2],
+            "example 3": VIGENERE_DATA2[3],
+        },
+    )
+    assert docs.responses == {
+        HTTPStatus.OK: ResponseInfo(
+            description="Success cipher with Vigenere algorithm.",
+            content=[
+                ContentInfo(
+                    type=VigenereData,
+                    examples=[
+                        ResponseExample(value=VIGENERE_DATA1[0]),
+                        ResponseExample(value=VIGENERE_DATA1[1]),
+                        ResponseExample(value=VIGENERE_DATA1[2]),
+                        ResponseExample(value=VIGENERE_DATA1[3]),
+                    ],
+                ),
+            ],
+        ),
+        HTTPStatus.BAD_REQUEST: "Bad request.",
+    }
+
+
+def test_operation_decipher() -> None:
+    docs = VigenereControllerDocs(Operation.DECIPHER)
+
+    assert docs.summary == "Apply the Vigenere algorithm to decipher the content."
+    assert (
+        docs.description
+        == "Use the key with the Vigenere algorithm to decipher the content."
+    )
+    assert "Vigenere" in docs.tags
+    assert docs.request_body == RequestBodyInfo(
+        description="Examples of requests body.",
+        examples={
+            "example 0": VIGENERE_DATA1[0],
+            "example 1": VIGENERE_DATA1[1],
+            "example 2": VIGENERE_DATA1[2],
+            "example 3": VIGENERE_DATA1[3],
+        },
+    )
+    assert docs.responses == {
+        HTTPStatus.OK: ResponseInfo(
+            description="Success decipher with Vigenere algorithm.",
+            content=[
+                ContentInfo(
+                    type=VigenereData,
+                    examples=[
+                        ResponseExample(value=VIGENERE_DATA2[0]),
+                        ResponseExample(value=VIGENERE_DATA2[1]),
+                        ResponseExample(value=VIGENERE_DATA2[2]),
+                        ResponseExample(value=VIGENERE_DATA2[3]),
+                    ],
+                ),
+            ],
+        ),
+        HTTPStatus.BAD_REQUEST: "Bad request.",
+    }
+
+
+@pytest.mark.raises(exception=OperationTypeError)
+def test_bad_type_operation() -> None:
+    VigenereControllerDocs("betet")
diff --git a/tests/api/v2/test_api.py b/tests/api/v2/test_api.py
new file mode 100644
index 0000000000000000000000000000000000000000..410cbab6358e0e71719942b9bb5bf6fdc0a37780
--- /dev/null
+++ b/tests/api/v2/test_api.py
@@ -0,0 +1,39 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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 blacksheep.testing import TestClient
+
+
+@pytest.mark.asyncio()
+async def test_get_api_docs(test_client: TestClient) -> None:
+    response = await test_client.get("/api/v2")
+
+    assert response is not None
+
+    assert response.status == 200
+    assert response.content is not None
+    assert response.reason.upper() == "OK"
+
+
+@pytest.mark.asyncio()
+async def test_get_api_redocs(test_client: TestClient) -> None:
+    response = await test_client.get("/api/v2/redocs")
+
+    assert response is not None
+
+    assert response.status == 200
+    assert response.content is not None
+    assert response.reason.upper() == "OK"
diff --git a/tests/api/v2/test_caesar.py b/tests/api/v2/test_caesar.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a45d0935bfb10538da6a4b563adbbbd8cc2c5c6
--- /dev/null
+++ b/tests/api/v2/test_caesar.py
@@ -0,0 +1,723 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+from typing import Any
+
+import pytest
+from blacksheep import Content
+from blacksheep.testing import TestClient
+from essentials.json import dumps
+from pydantic import BaseModel
+
+from vigenere_api.models import CaesarData
+
+
+def json_content(data: BaseModel) -> Content:
+    return Content(
+        b"application/json",
+        dumps(data, separators=(",", ":")).encode("utf8"),
+    )
+
+
+def bad_content(content: Any, key: Any) -> Content:
+    msg = '{"content": '
+    msg += f'"{content}"' if isinstance(content, str) else f"{content}"
+    msg += ', "key": '
+    msg += f'"{key}"' if isinstance(key, str) else f"{key}"
+    msg += "}"
+
+    return Content(
+        b"application/json",
+        msg.encode(),
+    )
+
+
+class CipherSuite:
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_with_int_key(test_client: TestClient) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key=2,
+        )
+
+        response = await test_client.post(
+            "/api/v2/caesar/cipher",
+            content=json_content(caesar_input),
+        )
+
+        assert response is not None
+
+        data = await response.json()
+        assert data is not None
+
+        ciphered_caesar = CaesarData.parse_obj(data)
+
+        assert ciphered_caesar.key == caesar_input.key
+        assert ciphered_caesar.content == "Vguv"
+
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_with_negative_int_key(test_client: TestClient) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key=-2,
+        )
+
+        response = await test_client.post(
+            "/api/v2/caesar/cipher",
+            content=json_content(caesar_input),
+        )
+
+        assert response is not None
+
+        data = await response.json()
+        assert data is not None
+
+        ciphered_caesar = CaesarData.parse_obj(data)
+
+        assert ciphered_caesar.key == caesar_input.key
+        assert ciphered_caesar.content == "Rcqr"
+
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_with_str_lower_key(test_client: TestClient) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key="c",
+        )
+
+        response = await test_client.post(
+            "/api/v2/caesar/cipher",
+            content=json_content(caesar_input),
+        )
+
+        assert response is not None
+
+        data = await response.json()
+        assert data is not None
+
+        ciphered_caesar = CaesarData.parse_obj(data)
+
+        assert ciphered_caesar.key == caesar_input.key
+        assert ciphered_caesar.content == "Vguv"
+
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_with_str_upper_key(test_client: TestClient) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key="C",
+        )
+
+        response = await test_client.post(
+            "/api/v2/caesar/cipher",
+            content=json_content(caesar_input),
+        )
+
+        assert response is not None
+
+        data = await response.json()
+        assert data is not None
+
+        ciphered_caesar = CaesarData.parse_obj(data)
+
+        assert ciphered_caesar.key == caesar_input.key
+        assert ciphered_caesar.content == "Vguv"
+
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_equality_between_keys(test_client: TestClient) -> None:
+        caesar_input1 = CaesarData(
+            content="Test",
+            key=2,
+        )
+
+        response1 = await test_client.post(
+            "/api/v2/caesar/cipher",
+            content=json_content(caesar_input1),
+        )
+
+        assert response1 is not None
+
+        data1 = await response1.json()
+        assert data1 is not None
+
+        ciphered_caesar1 = CaesarData.parse_obj(data1)
+
+        assert ciphered_caesar1.key == caesar_input1.key
+        assert ciphered_caesar1.content == "Vguv"
+
+        caesar_input2 = CaesarData(
+            content="Test",
+            key="c",
+        )
+
+        response2 = await test_client.post(
+            "/api/v2/caesar/cipher",
+            content=json_content(caesar_input2),
+        )
+
+        assert response2 is not None
+
+        data2 = await response2.json()
+        assert data2 is not None
+
+        ciphered_caesar2 = CaesarData.parse_obj(data2)
+
+        assert ciphered_caesar2.key == caesar_input2.key
+        assert ciphered_caesar2.content == "Vguv"
+
+        caesar_input3 = CaesarData(
+            content="Test",
+            key="C",
+        )
+
+        response3 = await test_client.post(
+            "/api/v2/caesar/cipher",
+            content=json_content(caesar_input3),
+        )
+
+        assert response3 is not None
+
+        data3 = await response3.json()
+        assert data3 is not None
+
+        ciphered_caesar3 = CaesarData.parse_obj(data3)
+
+        assert ciphered_caesar3.key == caesar_input3.key
+        assert ciphered_caesar3.content == "Vguv"
+
+        assert (
+            ciphered_caesar1.content
+            == ciphered_caesar2.content
+            == ciphered_caesar3.content
+        )
+
+    class BadCipherSuite:
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_missing_content(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/cipher",
+                content=Content(
+                    b"application/json",
+                    b'{"key": 2}',
+                ),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_missing_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/cipher",
+                content=Content(
+                    b"application/json",
+                    b'{"content": "test"}',
+                ),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_type_content(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/cipher",
+                content=bad_content(254, 2),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is 'int'. Please give a string.",
+                    "type": "type_error.contenttype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_empty_content(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/cipher",
+                content=bad_content("", 2),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is empty. Please give a not empty string.",
+                    "type": "value_error.emptycontent",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_type_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/cipher",
+                content=bad_content("Test", 25.8),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is 'float'. Please give a string or an integer.",
+                    "type": "type_error.keytype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_empty_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/cipher",
+                content=bad_content("Test", ""),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is empty. Please give a one character string or an integer.",
+                    "type": "value_error.emptykey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_too_long_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/cipher",
+                content=bad_content("Test", "TT"),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is too long. Please give a one character string or an "
+                    "integer.",
+                    "type": "value_error.toolongkey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_not_alpha_str_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/cipher",
+                content=bad_content("Test", "+"),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key '+' is invalid. Please give a string or an integer.",
+                    "type": "value_error.badkey",
+                },
+            ]
+
+
+class DecipherSuite:
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_with_int_key(test_client: TestClient) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key=2,
+        )
+
+        response = await test_client.post(
+            "/api/v2/caesar/decipher",
+            content=json_content(caesar_input),
+        )
+
+        assert response is not None
+
+        data = await response.json()
+        assert data is not None
+
+        deciphered_caesar = CaesarData.parse_obj(data)
+
+        assert deciphered_caesar.key == caesar_input.key
+        assert deciphered_caesar.content == "Rcqr"
+
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_with_str_lower_key(test_client: TestClient) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key="c",
+        )
+
+        response = await test_client.post(
+            "/api/v2/caesar/decipher",
+            content=json_content(caesar_input),
+        )
+
+        assert response is not None
+
+        data = await response.json()
+        assert data is not None
+
+        deciphered_caesar = CaesarData.parse_obj(data)
+
+        assert deciphered_caesar.key == caesar_input.key
+        assert deciphered_caesar.content == "Rcqr"
+
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_with_str_upper_key(test_client: TestClient) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key="C",
+        )
+
+        response = await test_client.post(
+            "/api/v2/caesar/decipher",
+            content=json_content(caesar_input),
+        )
+
+        assert response is not None
+
+        data = await response.json()
+        assert data is not None
+
+        deciphered_caesar = CaesarData.parse_obj(data)
+
+        assert deciphered_caesar.key == caesar_input.key
+        assert deciphered_caesar.content == "Rcqr"
+
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_equality_between_keys(test_client: TestClient) -> None:
+        caesar_input1 = CaesarData(
+            content="Test",
+            key=2,
+        )
+
+        response1 = await test_client.post(
+            "/api/v2/caesar/decipher",
+            content=json_content(caesar_input1),
+        )
+
+        assert response1 is not None
+
+        data1 = await response1.json()
+        assert data1 is not None
+
+        deciphered_caesar1 = CaesarData.parse_obj(data1)
+
+        assert deciphered_caesar1.key == caesar_input1.key
+        assert deciphered_caesar1.content == "Rcqr"
+
+        caesar_input2 = CaesarData(
+            content="Test",
+            key="c",
+        )
+
+        response2 = await test_client.post(
+            "/api/v2/caesar/decipher",
+            content=json_content(caesar_input2),
+        )
+
+        assert response2 is not None
+
+        data2 = await response2.json()
+        assert data2 is not None
+
+        deciphered_caesar2 = CaesarData.parse_obj(data2)
+
+        assert deciphered_caesar2.key == caesar_input2.key
+        assert deciphered_caesar2.content == "Rcqr"
+
+        caesar_input3 = CaesarData(
+            content="Test",
+            key="C",
+        )
+
+        response3 = await test_client.post(
+            "/api/v2/caesar/decipher",
+            content=json_content(caesar_input3),
+        )
+
+        assert response3 is not None
+
+        data3 = await response3.json()
+        assert data3 is not None
+
+        deciphered_caesar3 = CaesarData.parse_obj(data3)
+
+        assert deciphered_caesar3.key == caesar_input3.key
+        assert deciphered_caesar3.content == "Rcqr"
+
+        assert (
+            deciphered_caesar1.content
+            == deciphered_caesar2.content
+            == deciphered_caesar3.content
+        )
+
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_with_negative_int_key(test_client: TestClient) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key=-2,
+        )
+
+        response = await test_client.post(
+            "/api/v2/caesar/decipher",
+            content=json_content(caesar_input),
+        )
+
+        assert response is not None
+
+        data = await response.json()
+        assert data is not None
+
+        deciphered_caesar = CaesarData.parse_obj(data)
+
+        assert deciphered_caesar.key == caesar_input.key
+        assert deciphered_caesar.content == "Vguv"
+
+    class BadDecipherSuite:
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_missing_content(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/decipher",
+                content=Content(
+                    b"application/json",
+                    b'{"key": 2}',
+                ),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_missing_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/decipher",
+                content=Content(
+                    b"application/json",
+                    b'{"content": "test"}',
+                ),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_type_content(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/decipher",
+                content=bad_content(254, 2),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is 'int'. Please give a string.",
+                    "type": "type_error.contenttype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_empty_content(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/decipher",
+                content=bad_content("", 2),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is empty. Please give a not empty string.",
+                    "type": "value_error.emptycontent",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_type_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/decipher",
+                content=bad_content("Test", 25.8),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is 'float'. Please give a string or an integer.",
+                    "type": "type_error.keytype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_empty_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/decipher",
+                content=bad_content("Test", ""),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is empty. Please give a one character string or an integer.",
+                    "type": "value_error.emptykey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_too_long_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/decipher",
+                content=bad_content("Test", "TT"),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is too long. Please give a one character string or an "
+                    "integer.",
+                    "type": "value_error.toolongkey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_not_alpha_str_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/caesar/decipher",
+                content=bad_content("Test", "+"),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key '+' is invalid. Please give a string or an integer.",
+                    "type": "value_error.badkey",
+                },
+            ]
diff --git a/tests/api/v2/test_vigenere.py b/tests/api/v2/test_vigenere.py
new file mode 100644
index 0000000000000000000000000000000000000000..6c4a1aa34b8dc81bc4ccf572657cd36b34966b15
--- /dev/null
+++ b/tests/api/v2/test_vigenere.py
@@ -0,0 +1,675 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+from typing import Any
+
+import pytest
+from blacksheep import Content
+from blacksheep.testing import TestClient
+from essentials.json import dumps
+from pydantic import BaseModel
+
+from vigenere_api.models import VigenereData
+
+
+def json_content(data: BaseModel) -> Content:
+    return Content(
+        b"application/json",
+        dumps(data, separators=(",", ":")).encode("utf8"),
+    )
+
+
+def bad_content(content: Any, key: Any) -> Content:
+    msg = '{"content": '
+    msg += f'"{content}"' if isinstance(content, str) else f"{content}"
+    msg += ', "key": '
+    msg += f'"{key}"' if isinstance(key, str) else f"{key}"
+    msg += "}"
+
+    return Content(
+        b"application/json",
+        msg.encode(),
+    )
+
+
+class CipherSuite:
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_with_str_lower_key(test_client: TestClient) -> None:
+        vigenere_input = VigenereData(
+            content="Test",
+            key="ct",
+        )
+
+        response = await test_client.post(
+            "/api/v2/vigenere/cipher",
+            content=json_content(vigenere_input),
+        )
+
+        assert response is not None
+
+        data = await response.json()
+        assert data is not None
+
+        ciphered_vigenere = VigenereData.parse_obj(data)
+
+        assert ciphered_vigenere.key == vigenere_input.key
+        assert ciphered_vigenere.content == "Vxum"
+
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_with_str_upper_key(test_client: TestClient) -> None:
+        vigenere_input = VigenereData(
+            content="Test",
+            key="CT",
+        )
+
+        response = await test_client.post(
+            "/api/v2/vigenere/cipher",
+            content=json_content(vigenere_input),
+        )
+
+        assert response is not None
+
+        data = await response.json()
+        assert data is not None
+
+        ciphered_vigenere = VigenereData.parse_obj(data)
+
+        assert ciphered_vigenere.key == vigenere_input.key
+        assert ciphered_vigenere.content == "Vxum"
+
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_with_str_mixes_key(test_client: TestClient) -> None:
+        vigenere_input = VigenereData(
+            content="Test",
+            key="Ct",
+        )
+
+        response = await test_client.post(
+            "/api/v2/vigenere/cipher",
+            content=json_content(vigenere_input),
+        )
+
+        assert response is not None
+
+        data = await response.json()
+        assert data is not None
+
+        ciphered_vigenere = VigenereData.parse_obj(data)
+
+        assert ciphered_vigenere.key == vigenere_input.key
+        assert ciphered_vigenere.content == "Vxum"
+
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_equality_between_keys(test_client: TestClient) -> None:
+        vigenere_input1 = VigenereData(
+            content="Test",
+            key="ct",
+        )
+
+        response1 = await test_client.post(
+            "/api/v2/vigenere/cipher",
+            content=json_content(vigenere_input1),
+        )
+
+        assert response1 is not None
+
+        data1 = await response1.json()
+        assert data1 is not None
+
+        ciphered_vigenere1 = VigenereData.parse_obj(data1)
+
+        assert ciphered_vigenere1.key == vigenere_input1.key
+        assert ciphered_vigenere1.content == "Vxum"
+
+        vigenere_input2 = VigenereData(
+            content="Test",
+            key="cT",
+        )
+
+        response2 = await test_client.post(
+            "/api/v2/vigenere/cipher",
+            content=json_content(vigenere_input2),
+        )
+
+        assert response2 is not None
+
+        data2 = await response2.json()
+        assert data2 is not None
+
+        ciphered_vigenere2 = VigenereData.parse_obj(data2)
+
+        assert ciphered_vigenere2.key == vigenere_input2.key
+        assert ciphered_vigenere2.content == "Vxum"
+
+        vigenere_input3 = VigenereData(
+            content="Test",
+            key="CT",
+        )
+
+        response3 = await test_client.post(
+            "/api/v2/vigenere/cipher",
+            content=json_content(vigenere_input3),
+        )
+
+        assert response3 is not None
+
+        data3 = await response3.json()
+        assert data3 is not None
+
+        ciphered_vigenere3 = VigenereData.parse_obj(data3)
+
+        assert ciphered_vigenere3.key == vigenere_input3.key
+        assert ciphered_vigenere3.content == "Vxum"
+
+        assert (
+            ciphered_vigenere1.content
+            == ciphered_vigenere2.content
+            == ciphered_vigenere3.content
+        )
+
+    class BadCipherSuite:
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_missing_content(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/cipher",
+                content=Content(
+                    b"application/json",
+                    b'{"key": "ct"}',
+                ),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_missing_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/cipher",
+                content=Content(
+                    b"application/json",
+                    b'{"content": "test"}',
+                ),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_type_content(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/cipher",
+                content=bad_content(254, "tt"),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is 'int'. Please give a string.",
+                    "type": "type_error.contenttype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_empty_content(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/cipher",
+                content=bad_content("", "ty"),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is empty. Please give a not empty string.",
+                    "type": "value_error.emptycontent",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_type_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/cipher",
+                content=bad_content("Test", 25.8),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is 'float'. Please give a string.",
+                    "type": "type_error.keytype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_empty_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/cipher",
+                content=bad_content("Test", ""),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is empty. Please give a one character string or an integer.",
+                    "type": "value_error.emptykey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_too_short_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/cipher",
+                content=bad_content("Test", "T"),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is too short. Please give a string with more than one character.",
+                    "type": "value_error.tooshortkey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_not_alpha_str_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/cipher",
+                content=bad_content("Test", "+t"),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key '+t' is invalid. Please give a string.",
+                    "type": "value_error.badkey",
+                },
+            ]
+
+
+class DecipherSuite:
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_with_str_lower_key(test_client: TestClient) -> None:
+        vigenere_input = VigenereData(
+            content="Test",
+            key="ct",
+        )
+
+        response = await test_client.post(
+            "/api/v2/vigenere/decipher",
+            content=json_content(vigenere_input),
+        )
+
+        assert response is not None
+
+        data = await response.json()
+        assert data is not None
+
+        deciphered_vigenere = VigenereData.parse_obj(data)
+
+        assert deciphered_vigenere.key == vigenere_input.key
+        assert deciphered_vigenere.content == "Rlqa"
+
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_with_str_upper_key(test_client: TestClient) -> None:
+        vigenere_input = VigenereData(
+            content="Test",
+            key="CT",
+        )
+
+        response = await test_client.post(
+            "/api/v2/vigenere/decipher",
+            content=json_content(vigenere_input),
+        )
+
+        assert response is not None
+
+        data = await response.json()
+        assert data is not None
+
+        deciphered_vigenere = VigenereData.parse_obj(data)
+
+        assert deciphered_vigenere.key == vigenere_input.key
+        assert deciphered_vigenere.content == "Rlqa"
+
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_with_str_mixed_key(test_client: TestClient) -> None:
+        vigenere_input = VigenereData(
+            content="Test",
+            key="Ct",
+        )
+
+        response = await test_client.post(
+            "/api/v2/vigenere/decipher",
+            content=json_content(vigenere_input),
+        )
+
+        assert response is not None
+
+        data = await response.json()
+        assert data is not None
+
+        deciphered_vigenere = VigenereData.parse_obj(data)
+
+        assert deciphered_vigenere.key == vigenere_input.key
+        assert deciphered_vigenere.content == "Rlqa"
+
+    @staticmethod
+    @pytest.mark.asyncio()
+    async def test_equality_between_keys(test_client: TestClient) -> None:
+        vigenere_input1 = VigenereData(
+            content="Test",
+            key="ct",
+        )
+
+        response1 = await test_client.post(
+            "/api/v2/vigenere/decipher",
+            content=json_content(vigenere_input1),
+        )
+
+        assert response1 is not None
+
+        data1 = await response1.json()
+        assert data1 is not None
+
+        deciphered_vigenere1 = VigenereData.parse_obj(data1)
+
+        assert deciphered_vigenere1.key == vigenere_input1.key
+        assert deciphered_vigenere1.content == "Rlqa"
+
+        vigenere_input2 = VigenereData(
+            content="Test",
+            key="cT",
+        )
+
+        response2 = await test_client.post(
+            "/api/v2/vigenere/decipher",
+            content=json_content(vigenere_input2),
+        )
+
+        assert response2 is not None
+
+        data2 = await response2.json()
+        assert data2 is not None
+
+        deciphered_vigenere2 = VigenereData.parse_obj(data2)
+
+        assert deciphered_vigenere2.key == vigenere_input2.key
+        assert deciphered_vigenere2.content == "Rlqa"
+
+        vigenere_input3 = VigenereData(
+            content="Test",
+            key="CT",
+        )
+
+        response3 = await test_client.post(
+            "/api/v2/vigenere/decipher",
+            content=json_content(vigenere_input3),
+        )
+
+        assert response3 is not None
+
+        data3 = await response3.json()
+        assert data3 is not None
+
+        deciphered_vigenere3 = VigenereData.parse_obj(data3)
+
+        assert deciphered_vigenere3.key == vigenere_input3.key
+        assert deciphered_vigenere3.content == "Rlqa"
+
+        assert (
+            deciphered_vigenere1.content
+            == deciphered_vigenere2.content
+            == deciphered_vigenere3.content
+        )
+
+    class BadDecipherSuite:
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_missing_content(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/decipher",
+                content=Content(
+                    b"application/json",
+                    b'{"key": "tt"}',
+                ),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_missing_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/decipher",
+                content=Content(
+                    b"application/json",
+                    b'{"content": "test"}',
+                ),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_type_content(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/decipher",
+                content=bad_content(254, "tt"),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is 'int'. Please give a string.",
+                    "type": "type_error.contenttype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_empty_content(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/decipher",
+                content=bad_content("", "tt"),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is empty. Please give a not empty string.",
+                    "type": "value_error.emptycontent",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_type_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/decipher",
+                content=bad_content("Test", 25.8),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is 'float'. Please give a string.",
+                    "type": "type_error.keytype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_empty_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/decipher",
+                content=bad_content("Test", ""),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is empty. Please give a one character string or an integer.",
+                    "type": "value_error.emptykey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_too_short_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/decipher",
+                content=bad_content("Test", "T"),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is too short. Please give a string with more than one character.",
+                    "type": "value_error.tooshortkey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.asyncio()
+        async def test_bad_not_alpha_str_key(test_client: TestClient) -> None:
+            response = await test_client.post(
+                "/api/v2/vigenere/decipher",
+                content=bad_content("Test", "+t"),
+            )
+
+            assert response is not None
+
+            data = await response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key '+t' is invalid. Please give a string.",
+                    "type": "value_error.badkey",
+                },
+            ]
diff --git a/tests/integration/api/test_index.py b/tests/integration/api/test_index.py
index 800668315769af604083dac0995632d786c410fd..a960be25bf5c8d698fea8793358007a1f569a4fe 100644
--- a/tests/integration/api/test_index.py
+++ b/tests/integration/api/test_index.py
@@ -33,7 +33,7 @@ def test_get_index(server: str) -> None:
     assert response.is_redirect
 
     assert response.next is not None
-    assert response.next.path_url == "/api/v1"
+    assert response.next.path_url == "/api/v2"
 
 
 @pytest.mark.integration_test()
diff --git a/tests/integration/api/v1/test_caesar_integration.py b/tests/integration/api/v1/test_caesar_integration.py
index 913ed9cea5e91ae48616ae561e7c677a6895e67c..4ccce0ae255390ced57c716af1c22f97f7204750 100644
--- a/tests/integration/api/v1/test_caesar_integration.py
+++ b/tests/integration/api/v1/test_caesar_integration.py
@@ -371,7 +371,7 @@ class IntegrationCipherSuite:
             assert data == [
                 {
                     "loc": ["key"],
-                    "msg": "The key '+' is invalid. Please give an alphabetic one character string or an integer.",
+                    "msg": "The key '+' is invalid. Please give a string or an integer.",
                     "type": "value_error.badkey",
                 },
             ]
@@ -718,7 +718,7 @@ class IntegrationDecipherSuite:
             assert data == [
                 {
                     "loc": ["key"],
-                    "msg": "The key '+' is invalid. Please give an alphabetic one character string or an integer.",
+                    "msg": "The key '+' is invalid. Please give a string or an integer.",
                     "type": "value_error.badkey",
                 },
             ]
diff --git a/tests/integration/api/v2/__init__.py b/tests/integration/api/v2/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..bf8de0773dc8411127d6bbaa8262a9194c6fc4c0
--- /dev/null
+++ b/tests/integration/api/v2/__init__.py
@@ -0,0 +1,15 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/tests/integration/api/v2/test_api.py b/tests/integration/api/v2/test_api.py
new file mode 100644
index 0000000000000000000000000000000000000000..a02454c8d5d529cfeaf53f1e1245da821d84c55f
--- /dev/null
+++ b/tests/integration/api/v2/test_api.py
@@ -0,0 +1,53 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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
+import requests
+
+
+@pytest.mark.integration_test()
+def test_get_api_docs(server: str) -> None:
+    response = requests.get(
+        url=server + "/api/v2",
+        timeout=1,
+        allow_redirects=False,
+    )
+
+    assert response is not None
+
+    assert response.status_code == 200
+    assert response.content != b""
+    assert response.text != ""
+    assert not response.is_redirect
+
+    assert response.next is None
+
+
+@pytest.mark.integration_test()
+def test_get_api_redocs(server: str) -> None:
+    response = requests.get(
+        url=server + "/api/v2/redocs",
+        timeout=1,
+        allow_redirects=False,
+    )
+
+    assert response is not None
+
+    assert response.status_code == 200
+    assert response.content != b""
+    assert response.text != ""
+    assert not response.is_redirect
+
+    assert response.next is None
diff --git a/tests/integration/api/v2/test_caesar_integration.py b/tests/integration/api/v2/test_caesar_integration.py
new file mode 100644
index 0000000000000000000000000000000000000000..35a134b4a45f25bfb96308fca022017c19ebe532
--- /dev/null
+++ b/tests/integration/api/v2/test_caesar_integration.py
@@ -0,0 +1,724 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+from typing import Any
+
+import pytest
+import requests
+from pydantic import BaseModel
+
+from vigenere_api.models import CaesarData
+
+
+def json_data(data: BaseModel) -> dict[str, Any]:
+    return data.dict()
+
+
+def bad_content(content: Any, key: Any) -> dict[str, Any]:
+    return {"content": content, "key": key}
+
+
+class IntegrationCipherSuite:
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_with_int_key(server: str) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key=2,
+        )
+
+        response = requests.post(
+            url=server + "/api/v2/caesar/cipher",
+            json=json_data(caesar_input),
+            timeout=1,
+        )
+
+        assert response is not None
+
+        data = response.json()
+        assert data is not None
+
+        ciphered_caesar = CaesarData.parse_obj(data)
+
+        assert ciphered_caesar.key == caesar_input.key
+        assert ciphered_caesar.content == "Vguv"
+
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_with_negative_int_key(server: str) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key=-2,
+        )
+
+        response = requests.post(
+            url=server + "/api/v2/caesar/cipher",
+            json=json_data(caesar_input),
+            timeout=1,
+        )
+
+        assert response is not None
+
+        data = response.json()
+        assert data is not None
+
+        ciphered_caesar = CaesarData.parse_obj(data)
+
+        assert ciphered_caesar.key == caesar_input.key
+        assert ciphered_caesar.content == "Rcqr"
+
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_with_str_lower_key(server: str) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key="c",
+        )
+
+        response = requests.post(
+            url=server + "/api/v2/caesar/cipher",
+            json=json_data(caesar_input),
+            timeout=1,
+        )
+
+        assert response is not None
+
+        data = response.json()
+        assert data is not None
+
+        ciphered_caesar = CaesarData.parse_obj(data)
+
+        assert ciphered_caesar.key == caesar_input.key
+        assert ciphered_caesar.content == "Vguv"
+
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_with_str_upper_key(server: str) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key="C",
+        )
+
+        response = requests.post(
+            url=server + "/api/v2/caesar/cipher",
+            json=json_data(caesar_input),
+            timeout=1,
+        )
+
+        assert response is not None
+
+        data = response.json()
+        assert data is not None
+
+        ciphered_caesar = CaesarData.parse_obj(data)
+
+        assert ciphered_caesar.key == caesar_input.key
+        assert ciphered_caesar.content == "Vguv"
+
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_equality_between_keys(server: str) -> None:
+        caesar_input1 = CaesarData(
+            content="Test",
+            key=2,
+        )
+
+        response1 = requests.post(
+            url=server + "/api/v2/caesar/cipher",
+            json=json_data(caesar_input1),
+            timeout=1,
+        )
+
+        assert response1 is not None
+
+        data1 = response1.json()
+        assert data1 is not None
+
+        ciphered_caesar1 = CaesarData.parse_obj(data1)
+
+        assert ciphered_caesar1.key == caesar_input1.key
+        assert ciphered_caesar1.content == "Vguv"
+
+        caesar_input2 = CaesarData(
+            content="Test",
+            key="c",
+        )
+
+        response2 = requests.post(
+            url=server + "/api/v2/caesar/cipher",
+            json=json_data(caesar_input2),
+            timeout=1,
+        )
+
+        assert response2 is not None
+
+        data2 = response2.json()
+        assert data2 is not None
+
+        ciphered_caesar2 = CaesarData.parse_obj(data2)
+
+        assert ciphered_caesar2.key == caesar_input2.key
+        assert ciphered_caesar2.content == "Vguv"
+
+        caesar_input3 = CaesarData(
+            content="Test",
+            key="C",
+        )
+
+        response3 = requests.post(
+            url=server + "/api/v2/caesar/cipher",
+            json=json_data(caesar_input3),
+            timeout=1,
+        )
+
+        assert response3 is not None
+
+        data3 = response3.json()
+        assert data3 is not None
+
+        ciphered_caesar3 = CaesarData.parse_obj(data3)
+
+        assert ciphered_caesar3.key == caesar_input3.key
+        assert ciphered_caesar3.content == "Vguv"
+
+        assert (
+            ciphered_caesar1.content
+            == ciphered_caesar2.content
+            == ciphered_caesar3.content
+        )
+
+    class BadCipherSuite:
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_missing_content(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/cipher",
+                json={"key": 2},
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_missing_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/cipher",
+                json={"content": "test"},
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_type_content(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/cipher",
+                json=bad_content(254, 2),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is 'int'. Please give a string.",
+                    "type": "type_error.contenttype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_empty_content(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/cipher",
+                json=bad_content("", 2),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is empty. Please give a not empty string.",
+                    "type": "value_error.emptycontent",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_type_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/cipher",
+                json=bad_content("Test", 25.8),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is 'float'. Please give a string or an integer.",
+                    "type": "type_error.keytype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_empty_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/cipher",
+                json=bad_content("Test", ""),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is empty. Please give a one character string or an integer.",
+                    "type": "value_error.emptykey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_too_long_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/cipher",
+                json=bad_content("Test", "TT"),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is too long. Please give a one character string or an integer.",
+                    "type": "value_error.toolongkey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_not_alpha_str_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/cipher",
+                json=bad_content("Test", "+"),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key '+' is invalid. Please give a string or an integer.",
+                    "type": "value_error.badkey",
+                },
+            ]
+
+
+class IntegrationDecipherSuite:
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_with_int_key(server: str) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key=2,
+        )
+
+        response = requests.post(
+            url=server + "/api/v2/caesar/decipher",
+            json=json_data(caesar_input),
+            timeout=1,
+        )
+
+        assert response is not None
+
+        data = response.json()
+        assert data is not None
+
+        deciphered_caesar = CaesarData.parse_obj(data)
+
+        assert deciphered_caesar.key == caesar_input.key
+        assert deciphered_caesar.content == "Rcqr"
+
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_with_str_lower_key(server: str) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key="c",
+        )
+
+        response = requests.post(
+            url=server + "/api/v2/caesar/decipher",
+            json=json_data(caesar_input),
+            timeout=1,
+        )
+
+        assert response is not None
+
+        data = response.json()
+        assert data is not None
+
+        deciphered_caesar = CaesarData.parse_obj(data)
+
+        assert deciphered_caesar.key == caesar_input.key
+        assert deciphered_caesar.content == "Rcqr"
+
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_with_str_upper_key(server: str) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key="C",
+        )
+
+        response = requests.post(
+            url=server + "/api/v2/caesar/decipher",
+            json=json_data(caesar_input),
+            timeout=1,
+        )
+
+        assert response is not None
+
+        data = response.json()
+        assert data is not None
+
+        deciphered_caesar = CaesarData.parse_obj(data)
+
+        assert deciphered_caesar.key == caesar_input.key
+        assert deciphered_caesar.content == "Rcqr"
+
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_equality_between_keys(server: str) -> None:
+        caesar_input1 = CaesarData(
+            content="Test",
+            key=2,
+        )
+
+        response1 = requests.post(
+            url=server + "/api/v2/caesar/decipher",
+            json=json_data(caesar_input1),
+            timeout=1,
+        )
+
+        assert response1 is not None
+
+        data1 = response1.json()
+        assert data1 is not None
+
+        deciphered_caesar1 = CaesarData.parse_obj(data1)
+
+        assert deciphered_caesar1.key == caesar_input1.key
+        assert deciphered_caesar1.content == "Rcqr"
+
+        caesar_input2 = CaesarData(
+            content="Test",
+            key="c",
+        )
+
+        response2 = requests.post(
+            url=server + "/api/v2/caesar/decipher",
+            json=json_data(caesar_input2),
+            timeout=1,
+        )
+
+        assert response2 is not None
+
+        data2 = response2.json()
+        assert data2 is not None
+
+        deciphered_caesar2 = CaesarData.parse_obj(data2)
+
+        assert deciphered_caesar2.key == caesar_input2.key
+        assert deciphered_caesar2.content == "Rcqr"
+
+        caesar_input3 = CaesarData(
+            content="Test",
+            key="C",
+        )
+
+        response3 = requests.post(
+            url=server + "/api/v2/caesar/decipher",
+            json=json_data(caesar_input3),
+            timeout=1,
+        )
+
+        assert response3 is not None
+
+        data3 = response3.json()
+        assert data3 is not None
+
+        deciphered_caesar3 = CaesarData.parse_obj(data3)
+
+        assert deciphered_caesar3.key == caesar_input3.key
+        assert deciphered_caesar3.content == "Rcqr"
+
+        assert (
+            deciphered_caesar1.content
+            == deciphered_caesar2.content
+            == deciphered_caesar3.content
+        )
+
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_with_negative_int_key(server: str) -> None:
+        caesar_input = CaesarData(
+            content="Test",
+            key=-2,
+        )
+
+        response = requests.post(
+            url=server + "/api/v2/caesar/decipher",
+            json=json_data(caesar_input),
+            timeout=1,
+        )
+
+        assert response is not None
+
+        data = response.json()
+        assert data is not None
+
+        deciphered_caesar = CaesarData.parse_obj(data)
+
+        assert deciphered_caesar.key == caesar_input.key
+        assert deciphered_caesar.content == "Vguv"
+
+    class BadDecipherSuite:
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_missing_content(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/decipher",
+                json={"key": 2},
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_missing_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/decipher",
+                json={"content": "test"},
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_type_content(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/decipher",
+                json=bad_content(254, 2),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is 'int'. Please give a string.",
+                    "type": "type_error.contenttype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_empty_content(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/decipher",
+                json=bad_content("", 2),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is empty. Please give a not empty string.",
+                    "type": "value_error.emptycontent",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_type_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/decipher",
+                json=bad_content("Test", 25.8),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is 'float'. Please give a string or an integer.",
+                    "type": "type_error.keytype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_empty_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/decipher",
+                json=bad_content("Test", ""),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is empty. Please give a one character string or an integer.",
+                    "type": "value_error.emptykey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_too_long_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/decipher",
+                json=bad_content("Test", "TT"),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is too long. Please give a one character string or an integer.",
+                    "type": "value_error.toolongkey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_not_alpha_str_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/caesar/decipher",
+                json=bad_content("Test", "+"),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key '+' is invalid. Please give a string or an integer.",
+                    "type": "value_error.badkey",
+                },
+            ]
diff --git a/tests/integration/api/v2/test_vigenere_integration.py b/tests/integration/api/v2/test_vigenere_integration.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e0f10d4444e317de747b4434750e93aa20ffc5e
--- /dev/null
+++ b/tests/integration/api/v2/test_vigenere_integration.py
@@ -0,0 +1,676 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+from typing import Any
+
+import pytest
+import requests
+from pydantic import BaseModel
+
+from vigenere_api.models import VigenereData
+
+
+def json_data(data: BaseModel) -> dict[str, Any]:
+    return data.dict()
+
+
+def bad_content(content: Any, key: Any) -> dict[str, Any]:
+    return {"content": content, "key": key}
+
+
+class IntegrationCipherSuite:
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_with_str_lower_key(server: str) -> None:
+        vigenere_input = VigenereData(
+            content="Test",
+            key="ct",
+        )
+
+        response = requests.post(
+            url=server + "/api/v2/vigenere/cipher",
+            json=json_data(vigenere_input),
+            timeout=1,
+        )
+
+        assert response is not None
+
+        data = response.json()
+        assert data is not None
+
+        ciphered_vigenere = VigenereData.parse_obj(data)
+
+        assert ciphered_vigenere.key == vigenere_input.key
+        assert ciphered_vigenere.content == "Vxum"
+
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_with_str_upper_key(server: str) -> None:
+        vigenere_input = VigenereData(
+            content="Test",
+            key="CT",
+        )
+
+        response = requests.post(
+            url=server + "/api/v2/vigenere/cipher",
+            json=json_data(vigenere_input),
+            timeout=1,
+        )
+
+        assert response is not None
+
+        data = response.json()
+        assert data is not None
+
+        ciphered_vigenere = VigenereData.parse_obj(data)
+
+        assert ciphered_vigenere.key == vigenere_input.key
+        assert ciphered_vigenere.content == "Vxum"
+
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_with_str_mixed_key(server: str) -> None:
+        vigenere_input = VigenereData(
+            content="Test",
+            key="Ct",
+        )
+
+        response = requests.post(
+            url=server + "/api/v2/vigenere/cipher",
+            json=json_data(vigenere_input),
+            timeout=1,
+        )
+
+        assert response is not None
+
+        data = response.json()
+        assert data is not None
+
+        ciphered_vigenere = VigenereData.parse_obj(data)
+
+        assert ciphered_vigenere.key == vigenere_input.key
+        assert ciphered_vigenere.content == "Vxum"
+
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_equality_between_keys(server: str) -> None:
+        vigenere_input1 = VigenereData(
+            content="Test",
+            key="ct",
+        )
+
+        response1 = requests.post(
+            url=server + "/api/v2/vigenere/cipher",
+            json=json_data(vigenere_input1),
+            timeout=1,
+        )
+
+        assert response1 is not None
+
+        data1 = response1.json()
+        assert data1 is not None
+
+        ciphered_vigenere1 = VigenereData.parse_obj(data1)
+
+        assert ciphered_vigenere1.key == vigenere_input1.key
+        assert ciphered_vigenere1.content == "Vxum"
+
+        vigenere_input2 = VigenereData(
+            content="Test",
+            key="cT",
+        )
+
+        response2 = requests.post(
+            url=server + "/api/v2/vigenere/cipher",
+            json=json_data(vigenere_input2),
+            timeout=1,
+        )
+
+        assert response2 is not None
+
+        data2 = response2.json()
+        assert data2 is not None
+
+        ciphered_vigenere2 = VigenereData.parse_obj(data2)
+
+        assert ciphered_vigenere2.key == vigenere_input2.key
+        assert ciphered_vigenere2.content == "Vxum"
+
+        vigenere_input3 = VigenereData(
+            content="Test",
+            key="CT",
+        )
+
+        response3 = requests.post(
+            url=server + "/api/v2/vigenere/cipher",
+            json=json_data(vigenere_input3),
+            timeout=1,
+        )
+
+        assert response3 is not None
+
+        data3 = response3.json()
+        assert data3 is not None
+
+        ciphered_vigenere3 = VigenereData.parse_obj(data3)
+
+        assert ciphered_vigenere3.key == vigenere_input3.key
+        assert ciphered_vigenere3.content == "Vxum"
+
+        assert (
+            ciphered_vigenere1.content
+            == ciphered_vigenere2.content
+            == ciphered_vigenere3.content
+        )
+
+    class BadCipherSuite:
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_missing_content(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/cipher",
+                json={"key": "tt"},
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_missing_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/cipher",
+                json={"content": "test"},
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_type_content(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/cipher",
+                json=bad_content(254, "tt"),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is 'int'. Please give a string.",
+                    "type": "type_error.contenttype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_empty_content(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/cipher",
+                json=bad_content("", "tt"),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is empty. Please give a not empty string.",
+                    "type": "value_error.emptycontent",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_type_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/cipher",
+                json=bad_content("Test", 25.8),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is 'float'. Please give a string.",
+                    "type": "type_error.keytype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_empty_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/cipher",
+                json=bad_content("Test", ""),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is empty. Please give a one character string or an integer.",
+                    "type": "value_error.emptykey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_too_short_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/cipher",
+                json=bad_content("Test", "T"),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is too short. Please give a string with more than one character.",
+                    "type": "value_error.tooshortkey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_not_alpha_str_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/cipher",
+                json=bad_content("Test", "+t"),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key '+t' is invalid. Please give a string.",
+                    "type": "value_error.badkey",
+                },
+            ]
+
+
+class IntegrationDecipherSuite:
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_with_str_lower_key(server: str) -> None:
+        vigenere_input = VigenereData(
+            content="Test",
+            key="ct",
+        )
+
+        response = requests.post(
+            url=server + "/api/v2/vigenere/decipher",
+            json=json_data(vigenere_input),
+            timeout=1,
+        )
+
+        assert response is not None
+
+        data = response.json()
+        assert data is not None
+
+        deciphered_vigenere = VigenereData.parse_obj(data)
+
+        assert deciphered_vigenere.key == vigenere_input.key
+        assert deciphered_vigenere.content == "Rlqa"
+
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_with_str_upper_key(server: str) -> None:
+        vigenere_input = VigenereData(
+            content="Test",
+            key="CT",
+        )
+
+        response = requests.post(
+            url=server + "/api/v2/vigenere/decipher",
+            json=json_data(vigenere_input),
+            timeout=1,
+        )
+
+        assert response is not None
+
+        data = response.json()
+        assert data is not None
+
+        deciphered_vigenere = VigenereData.parse_obj(data)
+
+        assert deciphered_vigenere.key == vigenere_input.key
+        assert deciphered_vigenere.content == "Rlqa"
+
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_with_str_mixed_key(server: str) -> None:
+        vigenere_input = VigenereData(
+            content="Test",
+            key="Ct",
+        )
+
+        response = requests.post(
+            url=server + "/api/v2/vigenere/decipher",
+            json=json_data(vigenere_input),
+            timeout=1,
+        )
+
+        assert response is not None
+
+        data = response.json()
+        assert data is not None
+
+        deciphered_vigenere = VigenereData.parse_obj(data)
+
+        assert deciphered_vigenere.key == vigenere_input.key
+        assert deciphered_vigenere.content == "Rlqa"
+
+    @staticmethod
+    @pytest.mark.integration_test()
+    def test_equality_between_keys(server: str) -> None:
+        vigenere_input1 = VigenereData(
+            content="Test",
+            key="ct",
+        )
+
+        response1 = requests.post(
+            url=server + "/api/v2/vigenere/decipher",
+            json=json_data(vigenere_input1),
+            timeout=1,
+        )
+
+        assert response1 is not None
+
+        data1 = response1.json()
+        assert data1 is not None
+
+        deciphered_vigenere1 = VigenereData.parse_obj(data1)
+
+        assert deciphered_vigenere1.key == vigenere_input1.key
+        assert deciphered_vigenere1.content == "Rlqa"
+
+        vigenere_input2 = VigenereData(
+            content="Test",
+            key="cT",
+        )
+
+        response2 = requests.post(
+            url=server + "/api/v2/vigenere/decipher",
+            json=json_data(vigenere_input2),
+            timeout=1,
+        )
+
+        assert response2 is not None
+
+        data2 = response2.json()
+        assert data2 is not None
+
+        deciphered_vigenere2 = VigenereData.parse_obj(data2)
+
+        assert deciphered_vigenere2.key == vigenere_input2.key
+        assert deciphered_vigenere2.content == "Rlqa"
+
+        vigenere_input3 = VigenereData(
+            content="Test",
+            key="CT",
+        )
+
+        response3 = requests.post(
+            url=server + "/api/v2/vigenere/decipher",
+            json=json_data(vigenere_input3),
+            timeout=1,
+        )
+
+        assert response3 is not None
+
+        data3 = response3.json()
+        assert data3 is not None
+
+        deciphered_vigenere3 = VigenereData.parse_obj(data3)
+
+        assert deciphered_vigenere3.key == vigenere_input3.key
+        assert deciphered_vigenere3.content == "Rlqa"
+
+        assert (
+            deciphered_vigenere1.content
+            == deciphered_vigenere2.content
+            == deciphered_vigenere3.content
+        )
+
+    class BadDecipherSuite:
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_missing_content(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/decipher",
+                json={"key": "tt"},
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_missing_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/decipher",
+                json={"content": "test"},
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_type_content(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/decipher",
+                json=bad_content(254, "tt"),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is 'int'. Please give a string.",
+                    "type": "type_error.contenttype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_empty_content(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/decipher",
+                json=bad_content("", "tt"),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["content"],
+                    "msg": "The content is empty. Please give a not empty string.",
+                    "type": "value_error.emptycontent",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_type_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/decipher",
+                json=bad_content("Test", 25.8),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is 'float'. Please give a string.",
+                    "type": "type_error.keytype",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_empty_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/decipher",
+                json=bad_content("Test", ""),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is empty. Please give a one character string or an integer.",
+                    "type": "value_error.emptykey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_too_short_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/decipher",
+                json=bad_content("Test", "T"),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key is too short. Please give a string with more than one character.",
+                    "type": "value_error.tooshortkey",
+                },
+            ]
+
+        @staticmethod
+        @pytest.mark.integration_test()
+        def test_bad_not_alpha_str_key(server: str) -> None:
+            response = requests.post(
+                url=server + "/api/v2/vigenere/decipher",
+                json=bad_content("Test", "+t"),
+                timeout=1,
+            )
+
+            assert response is not None
+
+            data = response.json()
+            assert data is not None
+
+            assert data == [
+                {
+                    "loc": ["key"],
+                    "msg": "The key '+t' is invalid. Please give a string.",
+                    "type": "value_error.badkey",
+                },
+            ]
diff --git a/tests/models/test_base_data.py b/tests/models/test_base_data.py
new file mode 100644
index 0000000000000000000000000000000000000000..c9f1d66cd3fd70f39101e629a173000b78899ead
--- /dev/null
+++ b/tests/models/test_base_data.py
@@ -0,0 +1,45 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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 pydantic import ValidationError
+
+from vigenere_api.models.base_data import BaseData
+
+
+class CtorSuite:
+    @staticmethod
+    def test_with_text() -> None:
+        text = "Test"
+        data = BaseData(content=text)
+
+        assert data.content == text
+
+    @staticmethod
+    @pytest.mark.raises(exception=ValidationError)
+    def test_missing_content() -> None:
+        _ignored_data = BaseData()
+
+    @staticmethod
+    @pytest.mark.raises(exception=ValidationError)
+    def test_bad_type_content() -> None:
+        text = b"Test"
+        _ignored_data = BaseData(content=text)
+
+    @staticmethod
+    @pytest.mark.raises(exception=ValidationError)
+    def test_bad_empty_content() -> None:
+        text = ""
+        _ignored_data = BaseData(content=text)
diff --git a/tests/models/test_check_key.py b/tests/models/test_check_key.py
new file mode 100644
index 0000000000000000000000000000000000000000..4601a6af760fc4c29d15c563332a4fbc2cfb6e6d
--- /dev/null
+++ b/tests/models/test_check_key.py
@@ -0,0 +1,42 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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.check_key import check_key
+from vigenere_api.models.helpers.errors import BadKeyError, EmptyKeyError, KeyTypeError
+
+
+def test_with_key() -> None:
+    key = "zz"
+    check_key(key)
+
+
+@pytest.mark.raises(exception=KeyTypeError)
+def test_bad_type_key() -> None:
+    key = b"z"
+    check_key(key)
+
+
+@pytest.mark.raises(exception=EmptyKeyError)
+def test_bad_empty_key() -> None:
+    key = ""
+    check_key(key)
+
+
+@pytest.mark.raises(exception=BadKeyError)
+def test_bad_not_alpha_str_key() -> None:
+    key = "$z"
+    check_key(key)
diff --git a/tests/models/test_convert_key.py b/tests/models/test_convert_key.py
new file mode 100644
index 0000000000000000000000000000000000000000..030e0124c7c00abb163b317e65de4123edafab21
--- /dev/null
+++ b/tests/models/test_convert_key.py
@@ -0,0 +1,54 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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 import convert_key
+from vigenere_api.models.helpers.errors import (
+    BadKeyError,
+    EmptyKeyError,
+    KeyTypeError,
+    TooLongKeyError,
+)
+
+
+def test_convert_lower_key() -> None:
+    lower_index = convert_key("a")
+    assert lower_index == 0
+
+
+def test_convert_upper_key() -> None:
+    upper_index = convert_key("A")
+    assert upper_index == 0
+
+
+@pytest.mark.raises(exception=KeyTypeError)
+def test_bad_type_key() -> None:
+    _ignored = convert_key(b"ter")
+
+
+@pytest.mark.raises(exception=BadKeyError)
+def test_bad_key() -> None:
+    _ignored = convert_key("8")
+
+
+@pytest.mark.raises(exception=EmptyKeyError)
+def test_empty_key() -> None:
+    _ignored = convert_key("")
+
+
+@pytest.mark.raises(exception=TooLongKeyError)
+def test_too_long_key() -> None:
+    _ignored = convert_key("aa")
diff --git a/tests/models/test_helper.py b/tests/models/test_helper.py
deleted file mode 100644
index 0b35705d586473a3ce42f7ddcacb284aa4d50cf1..0000000000000000000000000000000000000000
--- a/tests/models/test_helper.py
+++ /dev/null
@@ -1,109 +0,0 @@
-# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-#  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/>.                         +
-# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
-"""Model helper tests."""
-
-import pytest
-
-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,
-)
-
-
-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_move_char.py b/tests/models/test_move_char.py
new file mode 100644
index 0000000000000000000000000000000000000000..d1f30f7de7b4733a305a4a74fdb819eab2917e85
--- /dev/null
+++ b/tests/models/test_move_char.py
@@ -0,0 +1,71 @@
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+#  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/>.                         +
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+"""Model helper tests."""
+
+import pytest
+
+from vigenere_api.models.helpers import move_char
+from vigenere_api.models.helpers.errors import (
+    HelperBadCharValueError,
+    HelperBadFirstLetterValueError,
+    HelperBadLengthCharValueError,
+    HelperCharTypeError,
+    HelperFirstLetterTypeError,
+    HelperKeyTypeError,
+)
+
+
+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")
diff --git a/tests/version/test_version.py b/tests/version/test_version.py
index 35bafec88b3ce74ed5bf41fe43eabaf1dfd2b7a7..8cdbc20f75451c976632342db55ec08f73bb9332 100644
--- a/tests/version/test_version.py
+++ b/tests/version/test_version.py
@@ -126,4 +126,4 @@ def test_get_version() -> None:
     v = get_version()
 
     assert isinstance(v, Version)
-    assert v == Version(major=1, minor=0, patch=0)
+    assert v == Version(major=2, minor=0, patch=0)