From 14da11acfc235d61646f6681e0cbc18a4c24afa9 Mon Sep 17 00:00:00 2001 From: Jeroen Vijgen Date: Sat, 25 Jan 2025 01:24:30 +0200 Subject: [PATCH] Setup testing, fix database connection, add manage.py --- api/asset_manager/src/database.py | 2 +- api/asset_manager/src/main.py | 30 +++--------- api/asset_manager/src/manage.py | 24 ++++++++++ .../models/1_20250123190326_second_try.py | 15 ++++++ .../src/{models.py => mixins/CMDMixin.py} | 4 +- .../src/{ => mixins}/__init__.py | 0 .../src/modules/assets/models.py | 3 +- .../src/modules/assets/router.py | 2 - api/asset_manager/src/modules/auth/models.py | 2 +- api/asset_manager/src/modules/auth/router.py | 15 +++--- api/asset_manager/src/modules/auth/utils.py | 3 +- .../src/modules/organizations/models.py | 12 ++--- .../src/modules/organizations/router.py | 2 +- api/asset_manager/src/modules/users/models.py | 2 +- api/asset_manager/src/modules/users/router.py | 11 ++++- api/asset_manager/src/pyproject.toml | 13 +++++ .../src/requirements/requirements.txt | 8 ++++ api/asset_manager/src/router.py | 14 ++++++ api/asset_manager/src/templates/email.html | 0 api/asset_manager/src/tests/__init__.py | 0 .../src/tests/fixtures/__init__.py | 0 .../src/tests/fixtures/conftest.py | 43 +++++++++++++++++ .../src/tests/test_accounts/__init__.py | 0 .../src/tests/test_authentication/__init__.py | 0 .../test_authentication.py | 48 +++++++++++++++++++ .../src/tests/test_general_routes/__init__.py | 0 .../test_general_routes/test_main_routes.py | 16 +++++++ 27 files changed, 218 insertions(+), 51 deletions(-) create mode 100644 api/asset_manager/src/manage.py create mode 100644 api/asset_manager/src/migrations/models/1_20250123190326_second_try.py rename api/asset_manager/src/{models.py => mixins/CMDMixin.py} (94%) rename api/asset_manager/src/{ => mixins}/__init__.py (100%) create mode 100644 api/asset_manager/src/router.py create mode 100644 api/asset_manager/src/templates/email.html create mode 100644 api/asset_manager/src/tests/__init__.py create mode 100644 api/asset_manager/src/tests/fixtures/__init__.py create mode 100644 api/asset_manager/src/tests/fixtures/conftest.py create mode 100644 api/asset_manager/src/tests/test_accounts/__init__.py create mode 100644 api/asset_manager/src/tests/test_authentication/__init__.py create mode 100644 api/asset_manager/src/tests/test_authentication/test_authentication.py create mode 100644 api/asset_manager/src/tests/test_general_routes/__init__.py create mode 100644 api/asset_manager/src/tests/test_general_routes/test_main_routes.py diff --git a/api/asset_manager/src/database.py b/api/asset_manager/src/database.py index ca197be6..373cb511 100644 --- a/api/asset_manager/src/database.py +++ b/api/asset_manager/src/database.py @@ -23,7 +23,7 @@ TORTOISE_ORM = { async def init_db(): - await Tortoise.init(db_url=settings.PSQL_CONNECT_STR, modules=modules) + await Tortoise.init(config=TORTOISE_ORM) async def migrate_db(): diff --git a/api/asset_manager/src/main.py b/api/asset_manager/src/main.py index 13b317f6..bab964b0 100644 --- a/api/asset_manager/src/main.py +++ b/api/asset_manager/src/main.py @@ -1,14 +1,13 @@ from fastapi import FastAPI -from fastapi.responses import JSONResponse -from starlette.responses import RedirectResponse -from tortoise import Tortoise +from tortoise import run_async from config import settings +from database import migrate_db + +from router import router as root_router from modules.assets.router import router as asset_router from modules.auth.router import router as auth_router from modules.users.router import router as users_router from modules.organizations.router import router as organizations_router -from tortoise.contrib.fastapi import register_tortoise -from database import modules app = FastAPI( title=settings.PROJECT_NAME, @@ -16,27 +15,10 @@ app = FastAPI( summary=settings.PROJECT_SUMMARY, ) -Tortoise.init_models(modules, "models") - -register_tortoise( - app, - db_url=settings.PSQL_CONNECT_STR, - modules=modules, - generate_schemas=True, - add_exception_handlers=True, -) +run_async(migrate_db()) +app.include_router(root_router) app.include_router(auth_router) app.include_router(users_router) app.include_router(organizations_router) app.include_router(asset_router) - - -@app.get("/") -async def main(): - return RedirectResponse(url="/docs") - - -@app.get("/ping") -async def ping() -> JSONResponse: - return JSONResponse("PONG") diff --git a/api/asset_manager/src/manage.py b/api/asset_manager/src/manage.py new file mode 100644 index 00000000..4c0525a7 --- /dev/null +++ b/api/asset_manager/src/manage.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +from ptpython.repl import embed # type: ignore + +from database import * + +import asyncio + + +async def setup(): + try: + await embed(globals=globals(), return_asyncio_coroutine=True, patch_stdout=True) + except EOFError: + loop.stop() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + asyncio.ensure_future(setup()) + loop.run_forever() + except KeyboardInterrupt: + pass diff --git a/api/asset_manager/src/migrations/models/1_20250123190326_second_try.py b/api/asset_manager/src/migrations/models/1_20250123190326_second_try.py new file mode 100644 index 00000000..c4617ab5 --- /dev/null +++ b/api/asset_manager/src/migrations/models/1_20250123190326_second_try.py @@ -0,0 +1,15 @@ +from tortoise import BaseDBAsyncClient + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + ALTER TABLE "asset" ADD "disabled_at" TIMESTAMPTZ; + ALTER TABLE "asset" ADD "modified_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP; + ALTER TABLE "asset" ADD "created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + ALTER TABLE "asset" DROP COLUMN "disabled_at"; + ALTER TABLE "asset" DROP COLUMN "modified_at"; + ALTER TABLE "asset" DROP COLUMN "created_at";""" diff --git a/api/asset_manager/src/models.py b/api/asset_manager/src/mixins/CMDMixin.py similarity index 94% rename from api/asset_manager/src/models.py rename to api/asset_manager/src/mixins/CMDMixin.py index 5edfdc3a..1d3b1b11 100644 --- a/api/asset_manager/src/models.py +++ b/api/asset_manager/src/mixins/CMDMixin.py @@ -1,7 +1,6 @@ from tortoise import fields - -class CMDMixin: +class CMDMixin(): """ Created, modified and delete mixin, these are required for every class. """ @@ -9,3 +8,4 @@ class CMDMixin: created_at = fields.DatetimeField(null=True, auto_now_add=True) modified_at = fields.DatetimeField(null=True, auto_now=True) disabled_at = fields.DatetimeField(null=True) + diff --git a/api/asset_manager/src/__init__.py b/api/asset_manager/src/mixins/__init__.py similarity index 100% rename from api/asset_manager/src/__init__.py rename to api/asset_manager/src/mixins/__init__.py diff --git a/api/asset_manager/src/modules/assets/models.py b/api/asset_manager/src/modules/assets/models.py index 581014bf..7b3bf32d 100644 --- a/api/asset_manager/src/modules/assets/models.py +++ b/api/asset_manager/src/modules/assets/models.py @@ -1,6 +1,7 @@ from tortoise.models import Model from tortoise import fields +from mixins.CMDMixin import CMDMixin -class Asset(Model): +class Asset(Model, CMDMixin): id = fields.UUIDField(primary_key=True) name = fields.CharField(max_length=128) diff --git a/api/asset_manager/src/modules/assets/router.py b/api/asset_manager/src/modules/assets/router.py index ded77ff1..00cc9ab0 100644 --- a/api/asset_manager/src/modules/assets/router.py +++ b/api/asset_manager/src/modules/assets/router.py @@ -2,8 +2,6 @@ from uuid import UUID from fastapi.routing import APIRouter -from modules.assets.models import Asset - router = APIRouter( prefix="/assets" ) diff --git a/api/asset_manager/src/modules/auth/models.py b/api/asset_manager/src/modules/auth/models.py index 31e50236..8598affa 100644 --- a/api/asset_manager/src/modules/auth/models.py +++ b/api/asset_manager/src/modules/auth/models.py @@ -3,7 +3,7 @@ from tortoise import fields import uuid from datetime import datetime -from models import CMDMixin +from mixins.CMDMixin import CMDMixin from config import settings diff --git a/api/asset_manager/src/modules/auth/router.py b/api/asset_manager/src/modules/auth/router.py index d9bc78e8..f9fee0e0 100644 --- a/api/asset_manager/src/modules/auth/router.py +++ b/api/asset_manager/src/modules/auth/router.py @@ -4,16 +4,15 @@ from fastapi.responses import JSONResponse from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from fastapi.routing import APIRouter -from utils import create_token -from models import Token +from modules.auth.utils import create_token +from modules.auth.models import Token from modules.users.models import User from fastapi import Depends, HTTPException from config import settings from tortoise.expressions import Q -from schemas import TokenModel -router = APIRouter(prefix="/auth") +router = APIRouter(prefix="/api/v1/auth", tags=["auth"]) oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") @@ -22,17 +21,17 @@ error: str = "E-Mail Address or password is incorrect" crypt = settings.CRYPT -@router.post("/", response_model=TokenModel) +@router.post("/", status_code=200) async def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]): - user: User = await User.filter( + user: User | None = await User.filter( Q(email=form.username) ).get_or_none() if user is None: - HTTPException(status_code=401, detail=error) + return HTTPException(status_code=401, detail=error) if user.check_against_password(form.password) is False: - HTTPException(status_code=401, detail=error) + return HTTPException(status_code=401, detail=error) return JSONResponse( await Token.create( diff --git a/api/asset_manager/src/modules/auth/utils.py b/api/asset_manager/src/modules/auth/utils.py index ffb9581f..ff2fdcc4 100644 --- a/api/asset_manager/src/modules/auth/utils.py +++ b/api/asset_manager/src/modules/auth/utils.py @@ -1,7 +1,6 @@ import uuid, time from config import settings from joserfc import jwt # type: ignore -from joserfc.jwt import OctKey # type: ignore crypt = settings.CRYPT @@ -21,7 +20,7 @@ def create_token(user_id: uuid, offset: float) -> str: "iat": curr_time, "exp": int(curr_time + offset), }, - OctKey.import_key(settings.SECRET_KEY), + settings.SECRET_KEY, ) diff --git a/api/asset_manager/src/modules/organizations/models.py b/api/asset_manager/src/modules/organizations/models.py index b3464e7e..48bc6d37 100644 --- a/api/asset_manager/src/modules/organizations/models.py +++ b/api/asset_manager/src/modules/organizations/models.py @@ -6,7 +6,7 @@ from tortoise.exceptions import ConfigurationError from tortoise.models import Model from tortoise import fields -from models import CMDMixin +from mixins.CMDMixin import CMDMixin from config import settings class EnumField(fields.CharField): @@ -45,11 +45,11 @@ class OrganizationType(Enum): There are no seat costs. """ - HOME: int = 1 # Home use (Any size) - SMALL_ORGANIZATION: int = 2 # 1-100 - MEDIUM_ORGANIZATION: int = 3 # 100 - 500 - LARGE_ORGANIZATION: int = 4 # 500 - 1000 - EXTRA_LARGE_ORGANIZATION: int = 5 # 1000 - 5000+ + HOME: str = "home" # Home use (Any size) + SMALL_ORGANIZATION: str = "s_org" # 1-100 + MEDIUM_ORGANIZATION: str = "m_org" # 100 - 500 + LARGE_ORGANIZATION: str = "l_org" # 500 - 1000 + EXTRA_LARGE_ORGANIZATION: str = "xl_org" # 1000 - 5000+ diff --git a/api/asset_manager/src/modules/organizations/router.py b/api/asset_manager/src/modules/organizations/router.py index b3467bae..9cb14a5b 100644 --- a/api/asset_manager/src/modules/organizations/router.py +++ b/api/asset_manager/src/modules/organizations/router.py @@ -1,7 +1,7 @@ from fastapi import APIRouter -router = APIRouter(prefix="/organizations") +router = APIRouter(prefix="/api/v1/organizations") @router.get("/") def all_organizations(): diff --git a/api/asset_manager/src/modules/users/models.py b/api/asset_manager/src/modules/users/models.py index e16a69c8..78aeff24 100644 --- a/api/asset_manager/src/modules/users/models.py +++ b/api/asset_manager/src/modules/users/models.py @@ -5,7 +5,7 @@ from tortoise.models import Model from tortoise import fields from modules.organizations.models import Organization -from models import CMDMixin +from mixins.CMDMixin import CMDMixin from config import settings crypt = settings.CRYPT diff --git a/api/asset_manager/src/modules/users/router.py b/api/asset_manager/src/modules/users/router.py index 85de7f37..1614e488 100644 --- a/api/asset_manager/src/modules/users/router.py +++ b/api/asset_manager/src/modules/users/router.py @@ -1,12 +1,19 @@ from fastapi import APIRouter -router = APIRouter(prefix="/users") +router = APIRouter(prefix="/api/v1/users", tags=["users"]) + @router.get("/") def get_all_users(): pass + +@router.post("/") +def create_user(): + pass + + @router.get("/me") def get_user(): - pass \ No newline at end of file + pass diff --git a/api/asset_manager/src/pyproject.toml b/api/asset_manager/src/pyproject.toml index dad15e41..0865fb21 100644 --- a/api/asset_manager/src/pyproject.toml +++ b/api/asset_manager/src/pyproject.toml @@ -1,4 +1,17 @@ +[tool.black] +exclude = '''/ + # Default values for Black. + \.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist| +/''' +line-length = 88 + [tool.aerich] tortoise_orm = "database.TORTOISE_ORM" location = "./migrations" src_folder = "./." + +[tool.pytest.ini_options] +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 a299c906..fca1a188 100644 --- a/api/asset_manager/src/requirements/requirements.txt +++ b/api/asset_manager/src/requirements/requirements.txt @@ -7,3 +7,11 @@ black>=24.10.0 joserfc>=1.0.1 passlib>=1.7.4 pytz>=2024.2 +ptpython>=0.25 + +# 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 diff --git a/api/asset_manager/src/router.py b/api/asset_manager/src/router.py new file mode 100644 index 00000000..00f8b7c9 --- /dev/null +++ b/api/asset_manager/src/router.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter +from fastapi.responses import JSONResponse, RedirectResponse + + +router = APIRouter(prefix="/api/v1") + +@router.get("/") +async def main(): + return RedirectResponse(url="/docs") + + +@router.get("/ping") +async def ping() -> JSONResponse: + return JSONResponse("PONG") diff --git a/api/asset_manager/src/templates/email.html b/api/asset_manager/src/templates/email.html new file mode 100644 index 00000000..e69de29b diff --git a/api/asset_manager/src/tests/__init__.py b/api/asset_manager/src/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/asset_manager/src/tests/fixtures/__init__.py b/api/asset_manager/src/tests/fixtures/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/asset_manager/src/tests/fixtures/conftest.py b/api/asset_manager/src/tests/fixtures/conftest.py new file mode 100644 index 00000000..393e5e5b --- /dev/null +++ b/api/asset_manager/src/tests/fixtures/conftest.py @@ -0,0 +1,43 @@ +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_accounts/__init__.py b/api/asset_manager/src/tests/test_accounts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/asset_manager/src/tests/test_authentication/__init__.py b/api/asset_manager/src/tests/test_authentication/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/asset_manager/src/tests/test_authentication/test_authentication.py b/api/asset_manager/src/tests/test_authentication/test_authentication.py new file mode 100644 index 00000000..59f357ee --- /dev/null +++ b/api/asset_manager/src/tests/test_authentication/test_authentication.py @@ -0,0 +1,48 @@ +from tests.fixtures.conftest import init_db +import pytest +from fastapi.testclient import TestClient +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 + + +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", + ) + 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() +# User.all().delete() +# ACL.all().delete() +# Membership.all().delete() + + +def test_read_main(): + response = client.post( + "/api/v1/auth", + data={ + "username": "admin@localhost.com", + "password": "password", + "grant_type": "password", + }, + ) + assert response.json() == {} + assert response.status_code == 200 diff --git a/api/asset_manager/src/tests/test_general_routes/__init__.py b/api/asset_manager/src/tests/test_general_routes/__init__.py new file mode 100644 index 00000000..e69de29b 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 new file mode 100644 index 00000000..73784753 --- /dev/null +++ b/api/asset_manager/src/tests/test_general_routes/test_main_routes.py @@ -0,0 +1,16 @@ +from fastapi.testclient import TestClient +from main import app + +client = TestClient(app) + +def setup_function(): + print("setting up") + +def test_read_main(): + response = client.get("/api/v1/") + assert response.status_code == 200 + +def test_get_pong(): + response = client.get("/api/v1/ping") + assert response.status_code == 200 + assert response.text == '"PONG"'