diff --git a/api/asset_manager/src/config.py b/api/asset_manager/src/config.py index 1dfd3aaa..731da4f4 100644 --- a/api/asset_manager/src/config.py +++ b/api/asset_manager/src/config.py @@ -9,7 +9,12 @@ class Settings(BaseSettings): PROJECT_SUMMARY: str = "Product API for StoneEdge." SECRET_KEY: str | None = None HASHING_SCHEME: str = "HS512" - PSQL_CONNECT_STR: str = "postgres://user:password@localhost:5432/stoneedge" + PSQL_USERNAME: str = "user" + PSQL_PASSWORD: str = "password" + PSQL_HOSTNAME: str = "localhost" + PSQL_PORT: int = 5432 + PSQL_DB_NAME: str = "stoneedge" + PSQL_TEST_DB_NAME: str = "stoneedge_testing" ACCESS_TOKEN_EXPIRE_MIN: int = 30 REFRESH_TOKEN_EXPIRE_MIN: int = 60 DEFAULT_TIMEZONE: str = pytz.UTC._tzname diff --git a/api/asset_manager/src/database.py b/api/asset_manager/src/database.py index 373cb511..8aa4382f 100644 --- a/api/asset_manager/src/database.py +++ b/api/asset_manager/src/database.py @@ -12,7 +12,18 @@ modules: dict[str, Any] = { } TORTOISE_ORM = { - "connections": {"default": settings.PSQL_CONNECT_STR}, + "connections": { + "default": { + "engine": "tortoise.backends.asyncpg", + "credentials": { + "host": settings.PSQL_HOSTNAME, + "database": settings.PSQL_DB_NAME, + "user": settings.PSQL_USERNAME, + "password": settings.PSQL_PASSWORD, + "port": settings.PSQL_PORT, + }, + } + }, "apps": { "models": { "models": modules.get("models", []) + ["aerich.models"], diff --git a/api/asset_manager/src/main.py b/api/asset_manager/src/main.py index bab964b0..6898f7d5 100644 --- a/api/asset_manager/src/main.py +++ b/api/asset_manager/src/main.py @@ -2,6 +2,7 @@ from fastapi import FastAPI from tortoise import run_async from config import settings from database import migrate_db +from responses import msgspec_jsonresponse from router import router as root_router from modules.assets.router import router as asset_router @@ -13,6 +14,7 @@ app = FastAPI( title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION, summary=settings.PROJECT_SUMMARY, + default_response_class=msgspec_jsonresponse ) run_async(migrate_db()) diff --git a/api/asset_manager/src/modules/auth/router.py b/api/asset_manager/src/modules/auth/router.py index f9fee0e0..d859c895 100644 --- a/api/asset_manager/src/modules/auth/router.py +++ b/api/asset_manager/src/modules/auth/router.py @@ -21,17 +21,17 @@ error: str = "E-Mail Address or password is incorrect" crypt = settings.CRYPT -@router.post("/", status_code=200) +@router.post("/") async def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]): user: User | None = await User.filter( Q(email=form.username) ).get_or_none() if user is None: - return HTTPException(status_code=401, detail=error) + raise HTTPException(status_code=401, detail=error) if user.check_against_password(form.password) is False: - return HTTPException(status_code=401, detail=error) + raise HTTPException(status_code=401, detail=error) return JSONResponse( await Token.create( diff --git a/api/asset_manager/src/modules/users/router.py b/api/asset_manager/src/modules/users/router.py index 1614e488..4418c8e8 100644 --- a/api/asset_manager/src/modules/users/router.py +++ b/api/asset_manager/src/modules/users/router.py @@ -1,5 +1,7 @@ from fastapi import APIRouter +from modules.users.models import User + router = APIRouter(prefix="/api/v1/users", tags=["users"]) @@ -8,7 +10,6 @@ router = APIRouter(prefix="/api/v1/users", tags=["users"]) def get_all_users(): pass - @router.post("/") def create_user(): pass diff --git a/api/asset_manager/src/pyproject.toml b/api/asset_manager/src/pyproject.toml index 0865fb21..b604f244 100644 --- a/api/asset_manager/src/pyproject.toml +++ b/api/asset_manager/src/pyproject.toml @@ -11,6 +11,7 @@ location = "./migrations" src_folder = "./." [tool.pytest.ini_options] +asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" testpaths = [ "tests/", diff --git a/api/asset_manager/src/requirements/requirements.txt b/api/asset_manager/src/requirements/requirements.txt index fca1a188..74e9dd4c 100644 --- a/api/asset_manager/src/requirements/requirements.txt +++ b/api/asset_manager/src/requirements/requirements.txt @@ -8,10 +8,13 @@ joserfc>=1.0.1 passlib>=1.7.4 pytz>=2024.2 ptpython>=0.25 +msgspec>=0.19.0 # Test Suite httpx>=0.28.1 pytest>=8.3.4 mock>=5.1.0 pytest-mock>=3.14.0 -anyio>=4.8.0 \ No newline at end of file +pytest-asyncio>=0.25.3 +asyncio>=3.4.3 +asgi-lifespan>=2.1.0 \ No newline at end of file diff --git a/api/asset_manager/src/responses.py b/api/asset_manager/src/responses.py new file mode 100644 index 00000000..ef2fdf3c --- /dev/null +++ b/api/asset_manager/src/responses.py @@ -0,0 +1,16 @@ +from typing import Any +from fastapi.responses import JSONResponse + +try: + import msgspec # type: ignore +except ImportError: # pragma: nocover + msgspec = None # type: ignore + +class msgspec_jsonresponse(JSONResponse): + """ + JSON Response using the high-performance msgspec lib to serialize data to JSON. + """ + + def render(self, content: Any) -> bytes: + assert msgspec is not None, "msgspec must be installed to use msgspec_jsonresponse" + return msgspec.json.encode(content) diff --git a/api/asset_manager/src/router.py b/api/asset_manager/src/router.py index 00f8b7c9..74db5eb4 100644 --- a/api/asset_manager/src/router.py +++ b/api/asset_manager/src/router.py @@ -5,7 +5,7 @@ from fastapi.responses import JSONResponse, RedirectResponse router = APIRouter(prefix="/api/v1") @router.get("/") -async def main(): +async def main() -> RedirectResponse: return RedirectResponse(url="/docs") diff --git a/api/asset_manager/src/tests/conftest.py b/api/asset_manager/src/tests/conftest.py new file mode 100644 index 00000000..00e78ee2 --- /dev/null +++ b/api/asset_manager/src/tests/conftest.py @@ -0,0 +1,85 @@ +from typing import AsyncGenerator, Optional, Self +import httpx +from tortoise import Tortoise +import pytest # type: ignore +from database import modules +from config import settings + +from asgi_lifespan import LifespanManager # type: ignore + +try: + from main import app +except ImportError: + import sys + from pathlib import Path + + sys.path.append(str(Path(__file__).parent.parent)) + from main import app + +TORTOISE_ORM = { + "connections": { + "default": { + "engine": "tortoise.backends.asyncpg", + "credentials": { + "host": settings.PSQL_HOSTNAME, + "database": settings.PSQL_TEST_DB_NAME, + "user": settings.PSQL_USERNAME, + "password": settings.PSQL_PASSWORD, + "port": settings.PSQL_PORT, + }, + } + }, + "apps": { + "models": { + "models": modules.get("models", []) + ["aerich.models"], + "default_connection": "default", + }, + }, +} + + +@pytest.fixture(scope="session") +def anyio_backend(): + return "asyncio" + + +class TestClient(httpx.AsyncClient): + def __init__(self, app, base_url="http://localhost", mount_lifespan=True, **kw) -> None: + self.mount_lifespan = mount_lifespan + self._manager: Optional[LifespanManager] = None + super().__init__(transport=httpx.ASGITransport(app), base_url=base_url, **kw) + + async def __aenter__(self) -> Self: + if self.mount_lifespan: + app = self._transport.app # type:ignore + self._manager = await LifespanManager(app).__aenter__() + self._transport = httpx.ASGITransport(app=self._manager.app) + return await super().__aenter__() + + async def __aexit__(self, *args, **kw): + await super().__aexit__(*args, **kw) + if self._manager is not None: + await self._manager.__aexit__(*args, **kw) + +async def init_db(create_db: bool = True, schemas: bool = True) -> None: + """Initial database connection""" + await Tortoise.init( + config=TORTOISE_ORM, timezone="Europe/Helsinki" + ) + if create_db: + print(f"Database created!") + if schemas: + await Tortoise.generate_schemas() + print("Success to generate schemas") + +@pytest.fixture(scope="session", autouse=True) +async def initialize_tests(): + await init_db() + yield + await Tortoise._drop_databases() + + +@pytest.fixture(scope="session") +async def client() -> AsyncGenerator[TestClient, None]: + async with TestClient(app) as c: + yield c \ No newline at end of file diff --git a/api/asset_manager/src/tests/fixtures/__init__.py b/api/asset_manager/src/tests/fixtures/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/api/asset_manager/src/tests/fixtures/conftest.py b/api/asset_manager/src/tests/fixtures/conftest.py deleted file mode 100644 index 393e5e5b..00000000 --- a/api/asset_manager/src/tests/fixtures/conftest.py +++ /dev/null @@ -1,43 +0,0 @@ -import pytest -from httpx import AsyncClient -from tortoise import Tortoise -from database import modules - -from main import app - -DB_URL = "sqlite://:memory:" - - -async def init_db(db_url, create_db: bool = True, schemas: bool = True) -> None: - """Initial database connection""" - await Tortoise.init( - db_url=db_url, modules={"models": modules}, _create_db=create_db - ) - if create_db: - print(f"Database created! {db_url = }") - if schemas: - await Tortoise.generate_schemas() - print("Success to generate schemas") - - -async def init(db_url: str = DB_URL): - await init_db(db_url, True, True) - - -@pytest.fixture(scope="session") -def anyio_backend(): - return "anyio" - - -@pytest.fixture(scope="session") -async def client(): - async with AsyncClient(app=app, base_url="http://test") as client: - print("Client is ready") - yield client - - -@pytest.fixture(scope="session", autouse=True) -async def initialize_tests(): - await init() - yield - await Tortoise._drop_databases() diff --git a/api/asset_manager/src/tests/test_authentication/test_authentication.py b/api/asset_manager/src/tests/test_authentication/test_authentication.py index 59f357ee..eb7387ba 100644 --- a/api/asset_manager/src/tests/test_authentication/test_authentication.py +++ b/api/asset_manager/src/tests/test_authentication/test_authentication.py @@ -1,32 +1,23 @@ -from tests.fixtures.conftest import init_db import pytest -from fastapi.testclient import TestClient +from httpx import AsyncClient from modules.organizations.models import Organization from modules.users.models import ACL, Membership, User -from main import app from config import settings - -client = TestClient(app) - crypt = settings.CRYPT - +@pytest.mark.anyio async def setup_function(): - init_db() org = await Organization.create(name="Admin's Organization", type="home") user = await User.create( email="admin@localhost.com", username="admin", name="admin", surname="admin", + password=crypt.hash("password") ) - user.set_password("password") - user.save() acl = await ACL.create(READ=True, WRITE=True, REPORT=True, MANAGE=True, ADMIN=True) await Membership.create(organization=org, user=user, acl=acl) - print(org, user, acl) - # def teardown_function(): # Organization.all().delete() @@ -34,10 +25,10 @@ async def setup_function(): # ACL.all().delete() # Membership.all().delete() - -def test_read_main(): - response = client.post( - "/api/v1/auth", +async def test_read_main(client: AsyncClient): + print("start") + response = await client.post( + "http://localhost/api/v1/auth", data={ "username": "admin@localhost.com", "password": "password", diff --git a/api/asset_manager/src/tests/test_general_routes/test_main_routes.py b/api/asset_manager/src/tests/test_general_routes/test_main_routes.py index 73784753..d40de3d3 100644 --- a/api/asset_manager/src/tests/test_general_routes/test_main_routes.py +++ b/api/asset_manager/src/tests/test_general_routes/test_main_routes.py @@ -1,16 +1,15 @@ -from fastapi.testclient import TestClient -from main import app +import pytest +from httpx import AsyncClient -client = TestClient(app) -def setup_function(): - print("setting up") +@pytest.mark.anyio +async def test_read_main(client: AsyncClient): + response = await client.get("http://localhost:8000/api/v1/") + assert response.status_code == 307 -def test_read_main(): - response = client.get("/api/v1/") - assert response.status_code == 200 -def test_get_pong(): - response = client.get("/api/v1/ping") +@pytest.mark.anyio +async def test_get_pong(client: AsyncClient): + response = await client.get("http://localhost:8000/api/v1/ping") assert response.status_code == 200 assert response.text == '"PONG"'