Integration tests #3

Merged
root merged 3 commits from integration-tests into main 2025-01-01 18:18:38 +00:00
15 changed files with 527 additions and 21 deletions

View file

@ -1,3 +0,0 @@
[flake8]
max-line-length = 88
extend-ignore = E203, B008

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
.env .*env
.idea/ .idea/
.nvim.lua .nvim.lua
.python-version .python-version

View file

@ -15,7 +15,10 @@ repos:
rev: 7.1.1 rev: 7.1.1
hooks: hooks:
- id: flake8 - id: flake8
args: [--config, .flake8] args:
- --max-line-length=88
- --extend-ignore=E203,B008
- --per-file-ignores=tests/factories.py:E701
additional_dependencies: additional_dependencies:
- flake8-bugbear - flake8-bugbear
- flake8-comprehensions - flake8-comprehensions

364
poetry.lock generated
View file

@ -32,6 +32,21 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)",
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
trio = ["trio (>=0.26.1)"] trio = ["trio (>=0.26.1)"]
[[package]]
name = "asttokens"
version = "3.0.0"
description = "Annotate AST trees with source code positions"
optional = false
python-versions = ">=3.8"
files = [
{file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"},
{file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"},
]
[package.extras]
astroid = ["astroid (>=2,<4)"]
test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"]
[[package]] [[package]]
name = "async-timeout" name = "async-timeout"
version = "5.0.1" version = "5.0.1"
@ -79,6 +94,17 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
[[package]]
name = "decorator"
version = "5.1.1"
description = "Decorators for Humans"
optional = false
python-versions = ">=3.5"
files = [
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
]
[[package]] [[package]]
name = "dnspython" name = "dnspython"
version = "2.7.0" version = "2.7.0"
@ -114,6 +140,35 @@ files = [
dnspython = ">=2.0.0" dnspython = ">=2.0.0"
idna = ">=2.0.0" idna = ">=2.0.0"
[[package]]
name = "executing"
version = "2.1.0"
description = "Get the currently executing AST node of a frame, and other information"
optional = false
python-versions = ">=3.8"
files = [
{file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"},
{file = "executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"},
]
[package.extras]
tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"]
[[package]]
name = "faker"
version = "33.1.0"
description = "Faker is a Python package that generates fake data for you."
optional = false
python-versions = ">=3.8"
files = [
{file = "Faker-33.1.0-py3-none-any.whl", hash = "sha256:d30c5f0e2796b8970de68978365247657486eb0311c5abe88d0b895b68dff05d"},
{file = "faker-33.1.0.tar.gz", hash = "sha256:1c925fc0e86a51fc46648b504078c88d0cd48da1da2595c4e712841cab43a1e4"},
]
[package.dependencies]
python-dateutil = ">=2.4"
typing-extensions = "*"
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.115.6" version = "0.115.6"
@ -402,6 +457,73 @@ files = [
[package.extras] [package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "ipython"
version = "8.31.0"
description = "IPython: Productive Interactive Computing"
optional = false
python-versions = ">=3.10"
files = [
{file = "ipython-8.31.0-py3-none-any.whl", hash = "sha256:46ec58f8d3d076a61d128fe517a51eb730e3aaf0c184ea8c17d16e366660c6a6"},
{file = "ipython-8.31.0.tar.gz", hash = "sha256:b6a2274606bec6166405ff05e54932ed6e5cfecaca1fc05f2cacde7bb074d70b"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
decorator = "*"
jedi = ">=0.16"
matplotlib-inline = "*"
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""}
prompt_toolkit = ">=3.0.41,<3.1.0"
pygments = ">=2.4.0"
stack_data = "*"
traitlets = ">=5.13.0"
typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""}
[package.extras]
all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"]
black = ["black"]
doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing_extensions"]
kernel = ["ipykernel"]
matplotlib = ["matplotlib"]
nbconvert = ["nbconvert"]
nbformat = ["nbformat"]
notebook = ["ipywidgets", "notebook"]
parallel = ["ipyparallel"]
qtconsole = ["qtconsole"]
test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"]
test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"]
[[package]]
name = "jedi"
version = "0.19.2"
description = "An autocompletion tool for Python that can be used for text editors."
optional = false
python-versions = ">=3.6"
files = [
{file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"},
{file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"},
]
[package.dependencies]
parso = ">=0.8.4,<0.9.0"
[package.extras]
docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"]
[[package]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.1.5" version = "3.1.5"
@ -513,6 +635,20 @@ files = [
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
] ]
[[package]]
name = "matplotlib-inline"
version = "0.1.7"
description = "Inline Matplotlib backend for Jupyter"
optional = false
python-versions = ">=3.8"
files = [
{file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"},
{file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"},
]
[package.dependencies]
traitlets = "*"
[[package]] [[package]]
name = "mdurl" name = "mdurl"
version = "0.1.2" version = "0.1.2"
@ -524,6 +660,124 @@ files = [
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
] ]
[[package]]
name = "packaging"
version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
[[package]]
name = "parso"
version = "0.8.4"
description = "A Python Parser"
optional = false
python-versions = ">=3.6"
files = [
{file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"},
{file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"},
]
[package.extras]
qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
testing = ["docopt", "pytest"]
[[package]]
name = "pexpect"
version = "4.9.0"
description = "Pexpect allows easy control of interactive console applications."
optional = false
python-versions = "*"
files = [
{file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
{file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"},
]
[package.dependencies]
ptyprocess = ">=0.5"
[[package]]
name = "pluggy"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "polyfactory"
version = "2.18.1"
description = "Mock data generation factories"
optional = false
python-versions = "<4.0,>=3.8"
files = [
{file = "polyfactory-2.18.1-py3-none-any.whl", hash = "sha256:1a2b0715e08bfe9f14abc838fc013ab8772cb90e66f2e601e15e1127f0bc1b18"},
{file = "polyfactory-2.18.1.tar.gz", hash = "sha256:17c9db18afe4fb8d7dd8e5ba296e69da0fcf7d0f3b63d1840eb10d135aed5aad"},
]
[package.dependencies]
faker = "*"
typing-extensions = ">=4.6.0"
[package.extras]
attrs = ["attrs (>=22.2.0)"]
beanie = ["beanie", "pydantic[email]"]
full = ["attrs", "beanie", "msgspec", "odmantic", "pydantic", "sqlalchemy"]
msgspec = ["msgspec"]
odmantic = ["odmantic (<1.0.0)", "pydantic[email]"]
pydantic = ["pydantic[email]"]
sqlalchemy = ["sqlalchemy (>=1.4.29)"]
[[package]]
name = "prompt-toolkit"
version = "3.0.48"
description = "Library for building powerful interactive command lines in Python"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"},
{file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"},
]
[package.dependencies]
wcwidth = "*"
[[package]]
name = "ptyprocess"
version = "0.7.0"
description = "Run a subprocess in a pseudo terminal"
optional = false
python-versions = "*"
files = [
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
]
[[package]]
name = "pure-eval"
version = "0.2.3"
description = "Safely evaluate AST nodes without side effects"
optional = false
python-versions = "*"
files = [
{file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"},
{file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"},
]
[package.extras]
tests = ["pytest"]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.10.4" version = "2.10.4"
@ -690,6 +944,58 @@ files = [
[package.extras] [package.extras]
windows-terminal = ["colorama (>=0.4.6)"] windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pytest"
version = "8.3.4"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.5,<2"
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-asyncio"
version = "0.25.0"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.9"
files = [
{file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"},
{file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"},
]
[package.dependencies]
pytest = ">=8.2,<9"
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
]
[package.dependencies]
six = ">=1.5"
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.0.1" version = "1.0.1"
@ -840,6 +1146,17 @@ files = [
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
] ]
[[package]]
name = "six"
version = "1.17.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
]
[[package]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.1" version = "1.3.1"
@ -851,6 +1168,25 @@ files = [
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
] ]
[[package]]
name = "stack-data"
version = "0.6.3"
description = "Extract data from python stack frames and tracebacks for informative displays"
optional = false
python-versions = "*"
files = [
{file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
{file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
]
[package.dependencies]
asttokens = ">=2.1.0"
executing = ">=1.2.0"
pure-eval = "*"
[package.extras]
tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
[[package]] [[package]]
name = "starlette" name = "starlette"
version = "0.41.3" version = "0.41.3"
@ -868,6 +1204,21 @@ anyio = ">=3.4.0,<5"
[package.extras] [package.extras]
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
[[package]]
name = "traitlets"
version = "5.14.3"
description = "Traitlets Python configuration system"
optional = false
python-versions = ">=3.8"
files = [
{file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"},
{file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"},
]
[package.extras]
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"]
[[package]] [[package]]
name = "typer" name = "typer"
version = "0.15.1" version = "0.15.1"
@ -1055,6 +1406,17 @@ files = [
[package.dependencies] [package.dependencies]
anyio = ">=3.0.0" anyio = ">=3.0.0"
[[package]]
name = "wcwidth"
version = "0.2.13"
description = "Measures the displayed width of unicode strings in a terminal"
optional = false
python-versions = "*"
files = [
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
[[package]] [[package]]
name = "websockets" name = "websockets"
version = "14.1" version = "14.1"
@ -1139,4 +1501,4 @@ hiredis = ["hiredis"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "1f2ca7562492fce7198a033828bce3cd8b9ed4cd38fc9e5c35784113bb816827" content-hash = "9f0f96c653fdc92b7a1ed18be32ff88814ac5188b207fda1ea7e672a7cd19d6c"

View file

@ -1,12 +1,17 @@
from fastapi import FastAPI from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from redis.asyncio import Redis
from pssecret.models import Secret, SecretSaveResult from pssecret.models import Secret, SecretSaveResult
from pssecret.redis_db import redis from pssecret.redis_db import get_redis
from pssecret.utils import get_new_key from pssecret.utils import save_secret
app = FastAPI() app = FastAPI()
RedisDep = Annotated[Redis, Depends(get_redis)]
@app.post( @app.post(
"/secret", "/secret",
@ -18,12 +23,9 @@ app = FastAPI()
), ),
response_model=SecretSaveResult, response_model=SecretSaveResult,
) )
async def set_secret(data: Secret): async def set_secret(data: Secret, redis: RedisDep) -> dict[str, str]:
new_key = await get_new_key()
await redis.setex(new_key, 60 * 60 * 24, data.data)
return { return {
"key": new_key, "key": await save_secret(data, redis),
} }
@ -38,8 +40,8 @@ async def set_secret(data: Secret):
response_model=Secret, response_model=Secret,
responses={404: {"description": "The item was not found"}}, responses={404: {"description": "The item was not found"}},
) )
async def get_secret(secret_key: str): async def get_secret(secret_key: str, redis: RedisDep) -> dict[str, bytes]:
data: str | None = await redis.getdel(secret_key) data: bytes | None = await redis.getdel(secret_key)
if data is None: if data is None:
raise HTTPException(404) raise HTTPException(404)

View file

@ -1,6 +1,11 @@
# noinspection PyUnresolvedReferences,PyProtectedMember # noinspection PyUnresolvedReferences,PyProtectedMember
from typing import Annotated
from fastapi import Depends
from redis import asyncio as aioredis from redis import asyncio as aioredis
from pssecret.settings import settings from pssecret.settings import Settings, get_settings
redis = aioredis.from_url(str(settings.redis_url))
def get_redis(settings: Annotated[Settings, Depends(get_settings)]) -> aioredis.Redis:
return aioredis.from_url(str(settings.redis_url))

View file

@ -6,4 +6,5 @@ class Settings(BaseSettings):
redis_url: RedisDsn = RedisDsn("redis://localhost") redis_url: RedisDsn = RedisDsn("redis://localhost")
settings = Settings() def get_settings() -> Settings:
return Settings()

View file

@ -1,11 +1,22 @@
from uuid import uuid4 from uuid import uuid4
from pssecret.redis_db import redis from redis.asyncio import Redis
from pssecret.models import Secret
async def get_new_key() -> str: async def get_new_key(redis: Redis) -> str:
"""Returns free Redis key"""
while True: while True:
new_key = str(uuid4()) new_key = str(uuid4())
if not await redis.exists(new_key): if not await redis.exists(new_key):
return new_key return new_key
async def save_secret(data: Secret, redis: Redis) -> str:
"""Save passed data, returns retrieval key"""
new_key = await get_new_key(redis)
await redis.setex(new_key, 60 * 60 * 24, data.data)
return new_key

View file

@ -33,7 +33,20 @@ hiredis = { version = "3.1.0", optional = true }
hiredis = ["hiredis"] hiredis = ["hiredis"]
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = ">=8.3.4,<9"
pytest-asyncio = "*"
polyfactory = ">=2.18,<3"
[tool.poetry.group.dev-extra.dependencies]
ipython = "^8.31.0"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.basedpyright]
reportUnknownMemberType = "none"
reportUnusedCallResult = "none"
[tool.pytest.ini_options]
asyncio_mode = "auto"

0
tests/__init__.py Normal file
View file

38
tests/conftest.py Normal file
View file

@ -0,0 +1,38 @@
from collections.abc import AsyncGenerator
import pytest
from fastapi.testclient import TestClient
from pydantic_settings import SettingsConfigDict
from redis import asyncio as aioredis
from pssecret.main import app
from pssecret.settings import Settings, get_settings
class TestSettings(Settings):
model_config = SettingsConfigDict(env_file=".test.env")
@pytest.fixture
def settings() -> Settings:
return TestSettings()
@pytest.fixture()
async def redis_server(settings: Settings) -> AsyncGenerator[aioredis.Redis]:
redis = await aioredis.from_url(str(settings.redis_url))
yield redis
await redis.flushdb()
def get_test_settings() -> Settings:
return TestSettings()
@pytest.fixture(scope="session")
def client() -> TestClient:
client_ = TestClient(app)
app.dependency_overrides[get_settings] = get_test_settings
return client_

6
tests/factories.py Normal file
View file

@ -0,0 +1,6 @@
from polyfactory.factories.pydantic_factory import ModelFactory
from pssecret.models import Secret
class SecretFactory(ModelFactory[Secret]): ...

View file

View file

@ -0,0 +1,33 @@
from fastapi.testclient import TestClient
from tests.factories import SecretFactory
def test_store_secret_returns_key(client: TestClient):
response = client.post("/secret", json=dict(SecretFactory().build()))
assert response.json()["key"]
def test_stored_secret_could_be_retrieved(client: TestClient):
secret = SecretFactory().build()
response = client.post("/secret", json=dict(secret))
retrieval_key: str = response.json()["key"]
response = client.get(f"/secret/{retrieval_key}")
assert response.json()["data"] == secret.data
def test_stored_secret_could_be_retrieved_only_once(client: TestClient):
secret = SecretFactory().build()
response = client.post("/secret", json=dict(secret))
retrieval_key: str = response.json()["key"]
client.get(f"/secret/{retrieval_key}")
response = client.get(f"/secret/{retrieval_key}")
assert response.status_code == 404
assert secret.data not in response.text

View file

@ -0,0 +1,35 @@
from unittest.mock import patch
from redis.asyncio import Redis
from pssecret.utils import get_new_key, save_secret
from ..factories import SecretFactory
async def test_get_new_key_returns_key(redis_server: Redis) -> None:
assert isinstance(await get_new_key(redis_server), str)
async def test_get_new_key_returns_free_key(redis_server: Redis) -> None:
new_key = await get_new_key(redis_server)
res = not await redis_server.exists(new_key)
assert res
@patch("pssecret.utils.uuid4", side_effect=("used", "free"))
async def test_get_new_key_skips_used_keys(_, redis_server: Redis) -> None:
await redis_server.set("used", "")
assert await get_new_key(redis_server) == "free"
async def test_save_secret_data(redis_server: Redis) -> None:
secret = SecretFactory.build()
key = await save_secret(secret, redis_server)
redis_data: bytes | None = await redis_server.get(key)
assert redis_data is not None
assert redis_data.decode() == secret.data