From 75bbe2760ce030c28403cef1391aa7b7ada9433f Mon Sep 17 00:00:00 2001
From: DAVID Axel <axel.david@etu.univ-amu.fr>
Date: Sun, 7 May 2023 10:04:49 +0200
Subject: [PATCH] feat: :sparkles: Add V2 API :sparkles:

---
 src/vigenere_api/api/app.py                   |  13 +-
 src/vigenere_api/api/helpers/__init__.py      |   9 +-
 src/vigenere_api/api/helpers/errors.py        |  41 +
 .../api/helpers/open_api_handler.py           |   3 +-
 .../api/helpers/open_api_route_filter.py      |   1 +
 .../api/helpers/operation_docs.py             | 164 ++++
 .../api/v1/controllers/caesar/docs.py         |  81 +-
 src/vigenere_api/api/v2/__init__.py           |  17 +
 .../api/v2/controllers/__init__.py            |  23 +
 .../api/v2/controllers/caesar/__init__.py     |  22 +
 .../api/v2/controllers/caesar/caesar.py       |  91 +++
 .../api/v2/controllers/caesar/caesar.pyi      |  24 +
 .../api/v2/controllers/vigenere/__init__.py   |  22 +
 .../api/v2/controllers/vigenere/docs.py       |  58 ++
 .../api/v2/controllers/vigenere/vigenere.py   |  87 +++
 .../api/v2/controllers/vigenere/vigenere.pyi  |  24 +
 src/vigenere_api/api/v2/openapi_docs.py       |  23 +
 src/vigenere_api/models/base_data.py          |  62 ++
 src/vigenere_api/models/caesar.py             |  40 +-
 src/vigenere_api/models/errors.py             |   7 +
 src/vigenere_api/models/helpers/__init__.py   |   4 +-
 src/vigenere_api/models/helpers/check_key.py  |  47 ++
 .../models/helpers/convert_key.py             |  53 ++
 src/vigenere_api/models/helpers/errors.py     |   2 +
 .../helpers/{helper.py => move_char.py}       |  45 --
 .../models/helpers/vigenere_key.py            |  19 +-
 src/vigenere_api/models/vigenere.py           |  40 +-
 src/vigenere_api/version/version.py           |   2 +-
 tests/api/helpers/test_operation_docs.py      | 246 ++++++
 tests/api/test_index.py                       |   2 +-
 tests/api/v1/docs/test_caesar.py              |  13 +-
 tests/api/v1/test_caesar.py                   |   9 +-
 tests/api/v2/__init__.py                      |  15 +
 tests/api/v2/docs/__init__.py                 |  15 +
 tests/api/v2/docs/test_vigenere.py            | 113 +++
 tests/api/v2/test_api.py                      |  39 +
 tests/api/v2/test_caesar.py                   | 723 +++++++++++++++++
 tests/api/v2/test_vigenere.py                 | 675 ++++++++++++++++
 tests/integration/api/test_index.py           |   2 +-
 .../api/v1/test_caesar_integration.py         |   4 +-
 tests/integration/api/v2/__init__.py          |  15 +
 tests/integration/api/v2/test_api.py          |  53 ++
 .../api/v2/test_caesar_integration.py         | 724 ++++++++++++++++++
 .../api/v2/test_vigenere_integration.py       | 676 ++++++++++++++++
 tests/models/test_base_data.py                |  45 ++
 tests/models/test_check_key.py                |  42 +
 tests/models/test_convert_key.py              |  54 ++
 tests/models/test_helper.py                   | 109 ---
 tests/models/test_move_char.py                |  71 ++
 tests/version/test_version.py                 |   2 +-
 50 files changed, 4331 insertions(+), 340 deletions(-)
 create mode 100644 src/vigenere_api/api/helpers/operation_docs.py
 create mode 100644 src/vigenere_api/api/v2/__init__.py
 create mode 100644 src/vigenere_api/api/v2/controllers/__init__.py
 create mode 100644 src/vigenere_api/api/v2/controllers/caesar/__init__.py
 create mode 100644 src/vigenere_api/api/v2/controllers/caesar/caesar.py
 create mode 100644 src/vigenere_api/api/v2/controllers/caesar/caesar.pyi
 create mode 100644 src/vigenere_api/api/v2/controllers/vigenere/__init__.py
 create mode 100644 src/vigenere_api/api/v2/controllers/vigenere/docs.py
 create mode 100644 src/vigenere_api/api/v2/controllers/vigenere/vigenere.py
 create mode 100644 src/vigenere_api/api/v2/controllers/vigenere/vigenere.pyi
 create mode 100644 src/vigenere_api/api/v2/openapi_docs.py
 create mode 100644 src/vigenere_api/models/base_data.py
 create mode 100644 src/vigenere_api/models/helpers/check_key.py
 create mode 100644 src/vigenere_api/models/helpers/convert_key.py
 rename src/vigenere_api/models/helpers/{helper.py => move_char.py} (76%)
 create mode 100644 tests/api/helpers/test_operation_docs.py
 create mode 100644 tests/api/v2/__init__.py
 create mode 100644 tests/api/v2/docs/__init__.py
 create mode 100644 tests/api/v2/docs/test_vigenere.py
 create mode 100644 tests/api/v2/test_api.py
 create mode 100644 tests/api/v2/test_caesar.py
 create mode 100644 tests/api/v2/test_vigenere.py
 create mode 100644 tests/integration/api/v2/__init__.py
 create mode 100644 tests/integration/api/v2/test_api.py
 create mode 100644 tests/integration/api/v2/test_caesar_integration.py
 create mode 100644 tests/integration/api/v2/test_vigenere_integration.py
 create mode 100644 tests/models/test_base_data.py
 create mode 100644 tests/models/test_check_key.py
 create mode 100644 tests/models/test_convert_key.py
 delete mode 100644 tests/models/test_helper.py
 create mode 100644 tests/models/test_move_char.py

diff --git a/src/vigenere_api/api/app.py b/src/vigenere_api/api/app.py
index 7dfd7f2..90cfcd5 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 98e67f3..436c1b5 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 fd3c76a..547fd22 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 b153e41..dc60aec 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 4dd7c99..45e2767 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 0000000..ce69b36
--- /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 a58153f..5b7ef9c 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 0000000..3cc3eaf
--- /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 0000000..10a9504
--- /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 0000000..4fb5e93
--- /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 0000000..fc73027
--- /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 0000000..868c77b
--- /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 0000000..5ee7f9e
--- /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 0000000..b78481c
--- /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 0000000..2ce74f7
--- /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 0000000..149794e
--- /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 0000000..9551fe9
--- /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 0000000..6d5c514
--- /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 82cb12a..6ec4c9b 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 76cf57b..5d1670b 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 35481ef..5e1f489 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 0000000..f89ec32
--- /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 0000000..c4b558c
--- /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 569950e..14c1d9f 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 a91e933..0a5c41f 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 db5f46b..118606d 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 17ca2aa..a31ca4e 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 775fa33..8f100df 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 0000000..0900718
--- /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 74f4522..7f3c9ec 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 da99866..34c8a88 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 adb97b7..90818ac 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 0000000..bf8de07
--- /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 0000000..bf8de07
--- /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 0000000..367a98a
--- /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 0000000..410cbab
--- /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 0000000..7a45d09
--- /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 0000000..6c4a1aa
--- /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 8006683..a960be2 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 913ed9c..4ccce0a 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 0000000..bf8de07
--- /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 0000000..a02454c
--- /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 0000000..35a134b
--- /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 0000000..6e0f10d
--- /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 0000000..c9f1d66
--- /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 0000000..4601a6a
--- /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 0000000..030e012
--- /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 0b35705..0000000
--- 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 0000000..d1f30f7
--- /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 35bafec..8cdbc20 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)
-- 
GitLab