Merge pull request 'add tests for project' (#1) from test into dev1

Reviewed-on: #1
This commit is contained in:
2025-08-24 09:52:07 +03:00
14 changed files with 262 additions and 10 deletions

102
poetry.lock generated
View File

@@ -129,12 +129,12 @@ version = "0.4.6"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main"] groups = ["main", "test"]
markers = "platform_system == \"Windows\""
files = [ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
markers = {main = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""}
[[package]] [[package]]
name = "dnspython" name = "dnspython"
@@ -290,6 +290,18 @@ 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.1.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.8"
groups = ["test"]
files = [
{file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
]
[[package]] [[package]]
name = "mako" name = "mako"
version = "1.3.10" version = "1.3.10"
@@ -381,6 +393,18 @@ files = [
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
] ]
[[package]]
name = "packaging"
version = "25.0"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["test"]
files = [
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
]
[[package]] [[package]]
name = "passlib" name = "passlib"
version = "1.7.4" version = "1.7.4"
@@ -399,6 +423,22 @@ bcrypt = ["bcrypt (>=3.1.0)"]
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
totp = ["cryptography"] totp = ["cryptography"]
[[package]]
name = "pluggy"
version = "1.6.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.9"
groups = ["test"]
files = [
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["coverage", "pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.11.7" version = "2.11.7"
@@ -557,6 +597,21 @@ gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"]
toml = ["tomli (>=2.0.1)"] toml = ["tomli (>=2.0.1)"]
yaml = ["pyyaml (>=6.0.1)"] yaml = ["pyyaml (>=6.0.1)"]
[[package]]
name = "pygments"
version = "2.19.2"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
groups = ["test"]
files = [
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]] [[package]]
name = "pyjwt" name = "pyjwt"
version = "2.10.1" version = "2.10.1"
@@ -575,6 +630,47 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]]
name = "pytest"
version = "8.4.1"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.9"
groups = ["test"]
files = [
{file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"},
{file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"},
]
[package.dependencies]
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
iniconfig = ">=1"
packaging = ">=20"
pluggy = ">=1.5,<2"
pygments = ">=2.7.2"
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-asyncio"
version = "1.1.0"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.9"
groups = ["test"]
files = [
{file = "pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf"},
{file = "pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea"},
]
[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]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.1.0" version = "1.1.0"
@@ -805,4 +901,4 @@ standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.12" python-versions = ">=3.12"
content-hash = "7f9ca5ce7505707747e59087ccbb804dc0fef135c963fd2d2ebc8c91285ec188" content-hash = "cc2947613c2711ad32ccfa1b8f04a0d8bad6043c6e52bffaff12761ec76cc805"

View File

@@ -91,3 +91,7 @@ ignore = [
[tool.ruff.format] [tool.ruff.format]
# Enable Ruff's formatter. # Enable Ruff's formatter.
docstring-code-format = true docstring-code-format = true
[tool.poetry.group.test.dependencies]
pytest = "^8.4.1"
pytest-asyncio = "^1.1.0"

3
pytest.ini Normal file
View File

@@ -0,0 +1,3 @@
[pytest]
pythonpath = . src
asyncio_mode = auto

View File

@@ -3,8 +3,7 @@ from typing import Annotated
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from src.api.dependacies.db_dep import sessionDep from src.api.dependacies.db_dep import sessionDep
from src.api.dependacies.user_dep import ActiveUser, CurrentOrAdminTask, TaskOwnerDep from src.api.dependacies.user_dep import ActiveUser, TaskOwnerDep
from src.schemas.auth import TokenData
from src.schemas.tasks import TaskADDRequest from src.schemas.tasks import TaskADDRequest
from src.services.tasks import TaskService from src.services.tasks import TaskService
from src.services.users import UserService from src.services.users import UserService
@@ -19,7 +18,9 @@ async def get_tasks(session: sessionDep, user: ActiveUser):
@router.get("/{id}") @router.get("/{id}")
async def get_task_id(id: int): ... async def get_task_id(session: sessionDep, id: int, _: TaskOwnerDep):
task = await TaskService(session).get_task(id)
return task
@router.post("/") @router.post("/")

View File

@@ -34,7 +34,7 @@ class AccessToken(BaseSettings):
model_config = SettingsConfigDict( model_config = SettingsConfigDict(
env_file=".env", env_file_encoding="utf-8", env_prefix="ACCESS_TOKEN_" env_file=".env", env_file_encoding="utf-8", env_prefix="ACCESS_TOKEN_"
) )
expire_minutes: int = 15 expire_minutes: int
secret_key: str secret_key: str
algorithm: str = "HS256" algorithm: str = "HS256"
token_type: str = "bearer" # noqa: S105 token_type: str = "bearer" # noqa: S105
@@ -43,7 +43,7 @@ class AccessToken(BaseSettings):
class Settings(BaseSettings): class Settings(BaseSettings):
api: ApiPrefix = ApiPrefix() api: ApiPrefix = ApiPrefix()
db: DbSettings = DbSettings() db: DbSettings = DbSettings()
access_token: AccessToken = AccessToken() access_token: AccessToken = AccessToken() # type: ignore
settings = Settings() settings = Settings()

View File

@@ -26,6 +26,7 @@ class BaseRepo(Generic[ModelType]):
statement = insert(self.model).values(data).returning(self.model) statement = insert(self.model).values(data).returning(self.model)
result = await self.session.execute(statement) result = await self.session.execute(statement)
obj: ModelType = result.scalar_one() obj: ModelType = result.scalar_one()
print(obj)
return obj return obj
async def get_one_or_none(self, **filter_by: Any) -> ModelType | None: async def get_one_or_none(self, **filter_by: Any) -> ModelType | None:

View File

@@ -19,7 +19,8 @@ class TaskService(BaseService):
return Task.model_validate(created_task_orm) return Task.model_validate(created_task_orm)
async def get_task(self, task_id: int): async def get_task(self, task_id: int):
return await self.session.task.get_one_or_none(id=task_id) task = await self.session.task.get_one_or_none(id=task_id)
return Task.model_validate(task)
async def delete_task(self, task_id: int): async def delete_task(self, task_id: int):
await self.session.task.delete_one(id=task_id) await self.session.task.delete_one(id=task_id)

0
tests/__init__.py Normal file
View File

77
tests/conftest.py Normal file
View File

@@ -0,0 +1,77 @@
import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy import NullPool, insert
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from src.api.dependacies.db_dep import get_db
from src.core.auth_manager import AuthManager
from src.core.database import Base
from src.core.db_manager import DBManager
from src.core.settings import settings
from src.main import app
from src.models import * # noqa: F403
engine_null_pool = create_async_engine(
"sqlite+aiosqlite:///tests/test_db.db", poolclass=NullPool
)
test_session_maker = async_sessionmaker(engine_null_pool, expire_on_commit=False)
class TestDBManager(DBManager):
def __init__(self):
self.session_factory = test_session_maker
async def get_test_db():
async with TestDBManager() as db:
yield db
@pytest.fixture(scope="function")
async def db():
async for db in get_test_db():
yield db
@pytest.fixture(scope="function")
async def ac():
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as ac:
yield ac
app.dependency_overrides[get_db] = get_test_db
@pytest.fixture(scope="session", autouse=True)
async def setup_database():
async with engine_null_pool.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
@pytest.fixture(scope="session", autouse=True)
async def add_admin(setup_database):
hashed_pass = AuthManager.get_password_hash("admin")
user_admin = {
"username": "admin",
"hashed_password": hashed_pass,
"is_superuser": True,
}
async with test_session_maker() as conn:
result = await conn.execute(
insert(UsersORM).values(user_admin).returning(UsersORM) # noqa: F405
)
await conn.commit()
admin = result.scalar_one()
assert admin.is_superuser is True
@pytest.fixture
def auth_token(ac, add_admin):
response = ac.post(
f"{settings.api.v1_login_url}/login",
data={"username": "admin", "password": "admin"},
)
return response.json()["access_token"]

View File

View File

@@ -0,0 +1,28 @@
from httpx import AsyncClient
from src.core.settings import settings
from src.schemas.users import User
async def test_registration(ac):
user = {"username": "kot", "email": "super@kot.ru", "password": "P@ssw0rd"}
result = await ac.post(
f"{settings.api.v1_login_url}/signup",
json=user,
)
assert result.status_code == 200
assert User.model_validate(result.json())
assert result.json()["is_active"]
async def test_login(ac: AsyncClient):
result = await ac.post(
f"{settings.api.v1_login_url}/login",
data={
"grant_type": "password",
"username": "kot",
"password": "P@ssw0rd",
},
)
assert result.status_code == 200
assert result.json().get("access_token")

View File

View File

@@ -0,0 +1,10 @@
from src.core.auth_manager import AuthManager
async def test_jwt():
token = AuthManager.create_access_token(
data={"id": 1, "sub": "testuser", "is_active": "True"}
)
assert token
encode_token = AuthManager.decode_access_token(token=token)
assert encode_token["id"] == 1 and encode_token["sub"] == "testuser"

View File

@@ -0,0 +1,31 @@
from typing import TYPE_CHECKING
from src.schemas.users import User
if TYPE_CHECKING:
from tests.conftest import TestDBManager
async def test_user_crud(db: "TestDBManager"):
data = {
"username": "test",
"hashed_password": "hashed_pass",
"email": "test@mail.ru",
"is_active": True,
"is_superuser": False,
}
user = await db.user.create_one(data=data)
assert user.username == data["username"]
filtered_user = await db.user.get_filtered(username=data["username"])
assert filtered_user[0] == user
new_user = User.model_validate(user)
new_user.username = "Test2"
new_user.email = None
await db.user.update_one(id=new_user.id, data=User.model_dump(new_user))
updated_user = await db.user.get_one_or_none(id=new_user.id)
assert updated_user
assert updated_user.username == new_user.username
assert not updated_user.email
await db.user.delete_one(id=updated_user.id)
delete_user = await db.user.get_one_or_none(id=new_user.id)
assert not delete_user