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