diff --git a/.gitignore b/.gitignore index 8d5292d1..fcf494be 100644 --- a/.gitignore +++ b/.gitignore @@ -92,5 +92,6 @@ /web/**/thumb /web/**/sketch -# Prevent uploading DB files -*.sqlite* \ No newline at end of file +# Prevent some sensitive files from being committed +*.sqlite* +.env \ No newline at end of file diff --git a/.tool-versions b/.tool-versions index d7b4824c..a1420ab1 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -python 3.13.1 -nodejs 23.4.0 +python 3.13.5t +nodejs 24.2.0 diff --git a/api/asset_manager/src/config.py b/api/asset_manager/src/config.py index 4f2424bf..40b8e6a8 100644 --- a/api/asset_manager/src/config.py +++ b/api/asset_manager/src/config.py @@ -9,12 +9,13 @@ class Settings(BaseSettings): PROJECT_SUMMARY: str = "Product API for StoneEdge." PROJECT_PUBLIC_URL: str = "localhost" SECRET_KEY: str | None = None + USE_HTTPS_ONLY: bool = False + IS_TESTING: bool = False 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 = 10 REFRESH_TOKEN_EXPIRE_MIN: int = 20 BACKEND_CORS_ORIGINS: list = ["*"] diff --git a/api/asset_manager/src/database.py b/api/asset_manager/src/database.py index df48d021..0c374e1a 100644 --- a/api/asset_manager/src/database.py +++ b/api/asset_manager/src/database.py @@ -5,14 +5,25 @@ from aerich import Command modules: dict[str, Any] = { "models": [ - "modules.assets.models", "modules.auth.models", "modules.users.models", "modules.organizations.models", ] } -TORTOISE_ORM = { +TEST_TORTOISE_ORM = { + "connections": { + "default": "sqlite://:memory:" + }, + "apps": { + "models": { + "models": modules.get("models", []) + ["aerich.models"], + "default_connection": "default", + }, + }, +} + +PROD_TORTOISE_ORM = { "connections": { "default": { "engine": "tortoise.backends.asyncpg", @@ -23,7 +34,7 @@ TORTOISE_ORM = { "password": settings.PSQL_PASSWORD, "port": settings.PSQL_PORT, }, - } + }, }, "apps": { "models": { @@ -34,11 +45,15 @@ TORTOISE_ORM = { } -async def migrate_db(): - aerich = Command(tortoise_config=TORTOISE_ORM) +async def migrate_db(tortoise_config=PROD_TORTOISE_ORM): + if settings.IS_TESTING: + tortoise_config=TEST_TORTOISE_ORM + aerich = Command(tortoise_config) await aerich.init() - await aerich.upgrade(run_in_transaction=True) - await Tortoise.init(config=TORTOISE_ORM) + await aerich.upgrade() + await Tortoise.init(tortoise_config) + await Tortoise.generate_schemas(safe=True) + async def end_connections_to_db(): - await Tortoise.close_connections() \ No newline at end of file + await Tortoise.close_connections() diff --git a/api/asset_manager/src/main.py b/api/asset_manager/src/main.py index dae869b3..4000414b 100644 --- a/api/asset_manager/src/main.py +++ b/api/asset_manager/src/main.py @@ -29,8 +29,14 @@ app = FastAPI( default_response_class=msgspec_jsonresponse, ) -app.add_middleware(HTTPSRedirectMiddleware) -app.add_middleware(TrustedHostMiddleware, allowed_hosts=[settings.PROJECT_PUBLIC_URL,]) +if settings.USE_HTTPS_ONLY: + app.add_middleware(HTTPSRedirectMiddleware) +app.add_middleware( + TrustedHostMiddleware, + allowed_hosts=[ + settings.PROJECT_PUBLIC_URL, + ], +) # Set all CORS enabled origins if settings.BACKEND_CORS_ORIGINS: diff --git a/api/asset_manager/src/manage.py b/api/asset_manager/src/manage.py index 4c0525a7..2b56356b 100644 --- a/api/asset_manager/src/manage.py +++ b/api/asset_manager/src/manage.py @@ -1,24 +1,103 @@ #!/usr/bin/env python3 -from ptpython.repl import embed # type: ignore +from ptpython.repl import embed, ReplExit -from database import * +import asyncio, importlib, contextlib, sys, os, tomllib, asyncclick -import asyncio +from database import migrate_db +from pathlib import Path +from asyncclick import BadOptionUsage, ClickException +from collections.abc import AsyncGenerator +from tortoise import Tortoise, connections +# +# Custom implementation of Tortoise CLI +# Original script is located under: https://github.com/tortoise/tortoise-cli +# License: Apache-2.0 as dictated as [here](https://github.com/tortoise/tortoise-cli/blob/main/LICENSE) +# + + +def tortoise_orm_config(file="pyproject.toml") -> str: + """ + get tortoise orm config from os environment variable or aerich item in pyproject.toml + + :param file: toml file that aerich item loads from it + :return: module path and var name that store the tortoise config, e.g.: 'settings.TORTOISE_ORM' + """ + if not (config := os.getenv("TORTOISE_ORM", "")) and (p := Path(file)).exists(): + doc = tomllib.loads(p.read_text("utf-8")) + config = doc.get("tool", {}).get("aerich", {}).get("tortoise_orm", "") + return config + + +def get_tortoise_config(config: str) -> dict: + """ + get tortoise config from module + :param ctx: + :param config: + :return: + """ + splits = config.split(".") + config_path = ".".join(splits[:-1]) + tortoise_config = splits[-1] + + try: + config_module = importlib.import_module(config_path) + except ModuleNotFoundError as e: + raise ClickException( + f"Error while importing configuration module: {e}" + ) from None + c = getattr(config_module, tortoise_config, None) + if not c: + raise BadOptionUsage( + option_name="--config", + message=f'Can\'t get "{tortoise_config}" from module "{config_module}"', + ctx=None, + ) + return c + + +@contextlib.asynccontextmanager +async def aclose_tortoise() -> AsyncGenerator[None]: + try: + yield + finally: + if Tortoise._inited: + await connections.close_all() + +def history(): + import readline + for i in range(1, readline.get_current_history_length()+1): + print("%3d %s" % (i, readline.get_history_item(i))) async def setup(): - try: - await embed(globals=globals(), return_asyncio_coroutine=True, patch_stdout=True) - except EOFError: - loop.stop() + if not (config := tortoise_orm_config()): + raise asyncclick.UsageError( + "You must specify TORTOISE_ORM in option or env, or config file pyproject.toml from config of aerich", + ctx=None, + ) + await migrate_db(get_tortoise_config(config)) + + async with aclose_tortoise(): + await embed( + globals=globals(), + title="shell", + vi_mode=True, + return_asyncio_coroutine=True, + patch_stdout=True, + ) if __name__ == "__main__": + if sys.path[0] != ".": + sys.path.insert(0, ".") + loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: - asyncio.ensure_future(setup()) - loop.run_forever() - except KeyboardInterrupt: - pass + history() + loop.run_until_complete(asyncio.ensure_future(setup())) + except (KeyboardInterrupt, ReplExit) as e: + print(e) + loop.stop() + diff --git a/api/asset_manager/src/migrations/models/0_20250122175143_init.py b/api/asset_manager/src/migrations/models/0_20250122175143_init.py deleted file mode 100644 index 88e0a6d0..00000000 --- a/api/asset_manager/src/migrations/models/0_20250122175143_init.py +++ /dev/null @@ -1,75 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - CREATE TABLE IF NOT EXISTS "asset" ( - "id" UUID NOT NULL PRIMARY KEY, - "name" VARCHAR(128) NOT NULL -); -CREATE TABLE IF NOT EXISTS "acl" ( - "id" UUID NOT NULL PRIMARY KEY, - "READ" BOOL NOT NULL DEFAULT False, - "WRITE" BOOL NOT NULL DEFAULT False, - "REPORT" BOOL NOT NULL DEFAULT False, - "MANAGE" BOOL NOT NULL DEFAULT False, - "ADMIN" BOOL NOT NULL DEFAULT False -); -COMMENT ON TABLE "acl" IS 'ACL'; -CREATE TABLE IF NOT EXISTS "organization" ( - "created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - "modified_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - "disabled_at" TIMESTAMPTZ, - "id" UUID NOT NULL PRIMARY KEY, - "name" VARCHAR(128) NOT NULL, - "type" VARCHAR(128) NOT NULL, - "disabled" BOOL NOT NULL DEFAULT False -); -COMMENT ON TABLE "organization" IS 'Organization'; -CREATE TABLE IF NOT EXISTS "user" ( - "created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - "modified_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - "disabled_at" TIMESTAMPTZ, - "id" UUID NOT NULL PRIMARY KEY, - "email" VARCHAR(128) NOT NULL, - "username" TEXT NOT NULL, - "name" TEXT NOT NULL, - "surname" TEXT NOT NULL, - "password" VARCHAR(128), - "disabled" BOOL NOT NULL DEFAULT False -); -COMMENT ON TABLE "user" IS 'User'; -CREATE TABLE IF NOT EXISTS "token" ( - "created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - "modified_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - "disabled_at" TIMESTAMPTZ, - "id" UUID NOT NULL PRIMARY KEY, - "token_type" VARCHAR(128) NOT NULL DEFAULT 'Bearer', - "access_token" VARCHAR(128), - "refresh_token" VARCHAR(128), - "disabled" BOOL NOT NULL DEFAULT False, - "user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE -); -COMMENT ON TABLE "token" IS 'Token'; -CREATE TABLE IF NOT EXISTS "membership" ( - "created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - "modified_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - "disabled_at" TIMESTAMPTZ, - "id" UUID NOT NULL PRIMARY KEY, - "disabled" BOOL NOT NULL DEFAULT False, - "acl_id" UUID NOT NULL REFERENCES "acl" ("id") ON DELETE CASCADE, - "organization_id" UUID NOT NULL REFERENCES "organization" ("id") ON DELETE CASCADE, - "user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE -); -COMMENT ON TABLE "membership" IS 'Membership'; -CREATE TABLE IF NOT EXISTS "aerich" ( - "id" SERIAL NOT NULL PRIMARY KEY, - "version" VARCHAR(255) NOT NULL, - "app" VARCHAR(100) NOT NULL, - "content" JSONB NOT NULL -);""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - """ diff --git a/api/asset_manager/src/migrations/models/0_20250623123107_init.py b/api/asset_manager/src/migrations/models/0_20250623123107_init.py new file mode 100644 index 00000000..92eb1c66 --- /dev/null +++ b/api/asset_manager/src/migrations/models/0_20250623123107_init.py @@ -0,0 +1,76 @@ +from tortoise import BaseDBAsyncClient + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + CREATE TABLE IF NOT EXISTS "acl" ( + "id" CHAR(36) NOT NULL PRIMARY KEY, + "READ" INT NOT NULL DEFAULT 0, + "WRITE" INT NOT NULL DEFAULT 0, + "REPORT" INT NOT NULL DEFAULT 0, + "MANAGE" INT NOT NULL DEFAULT 0, + "ADMIN" INT NOT NULL DEFAULT 0 +) /* ACL */; +CREATE TABLE IF NOT EXISTS "organization" ( + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "disabled_at" TIMESTAMP, + "id" CHAR(36) NOT NULL PRIMARY KEY, + "name" VARCHAR(128) NOT NULL, + "type" VARCHAR(128) NOT NULL, + "disabled" INT NOT NULL DEFAULT 0 +) /* Organization */; +CREATE TABLE IF NOT EXISTS "user" ( + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "disabled_at" TIMESTAMP, + "id" CHAR(36) NOT NULL PRIMARY KEY, + "email" VARCHAR(128) NOT NULL, + "username" TEXT NOT NULL, + "name" TEXT NOT NULL, + "surname" TEXT NOT NULL, + "password" VARCHAR(128), + "disabled" INT NOT NULL DEFAULT 0 +) /* User */; +CREATE TABLE IF NOT EXISTS "token" ( + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "disabled_at" TIMESTAMP, + "id" CHAR(36) NOT NULL PRIMARY KEY, + "token_type" VARCHAR(128) NOT NULL DEFAULT 'Bearer', + "access_token" TEXT, + "refresh_token" TEXT, + "disabled" INT NOT NULL DEFAULT 0, + "user_id" CHAR(36) NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE +) /* Token */; +CREATE TABLE IF NOT EXISTS "membership" ( + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "disabled_at" TIMESTAMP, + "id" CHAR(36) NOT NULL PRIMARY KEY, + "disabled" INT NOT NULL DEFAULT 0, + "acl_id" CHAR(36) NOT NULL REFERENCES "acl" ("id") ON DELETE CASCADE, + "organization_id" CHAR(36) NOT NULL REFERENCES "organization" ("id") ON DELETE CASCADE, + "user_id" CHAR(36) NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE +) /* Membership */; +CREATE TABLE IF NOT EXISTS "aerich" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "version" VARCHAR(255) NOT NULL, + "app" VARCHAR(100) NOT NULL, + "content" JSON NOT NULL +); +CREATE TABLE IF NOT EXISTS "Membership" ( + "organization_id" CHAR(36) NOT NULL REFERENCES "organization" ("id") ON DELETE NO ACTION, + "user_id" CHAR(36) NOT NULL REFERENCES "user" ("id") ON DELETE NO ACTION +); +CREATE UNIQUE INDEX IF NOT EXISTS "uidx_Membership_organiz_b0a446" ON "Membership" ("organization_id", "user_id"); +CREATE TABLE IF NOT EXISTS "Membership" ( + "user_id" CHAR(36) NOT NULL REFERENCES "user" ("id") ON DELETE NO ACTION, + "organization_id" CHAR(36) NOT NULL REFERENCES "organization" ("id") ON DELETE NO ACTION +); +CREATE UNIQUE INDEX IF NOT EXISTS "uidx_Membership_user_id_cc48d3" ON "Membership" ("user_id", "organization_id");""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + """ 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 deleted file mode 100644 index c4617ab5..00000000 --- a/api/asset_manager/src/migrations/models/1_20250123190326_second_try.py +++ /dev/null @@ -1,15 +0,0 @@ -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/migrations/models/2_20250214131414_update.py b/api/asset_manager/src/migrations/models/2_20250214131414_update.py deleted file mode 100644 index 3b9f1a5e..00000000 --- a/api/asset_manager/src/migrations/models/2_20250214131414_update.py +++ /dev/null @@ -1,13 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "token" ALTER COLUMN "refresh_token" TYPE TEXT USING "refresh_token"::TEXT; - ALTER TABLE "token" ALTER COLUMN "access_token" TYPE TEXT USING "access_token"::TEXT;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "token" ALTER COLUMN "refresh_token" TYPE VARCHAR(128) USING "refresh_token"::VARCHAR(128); - ALTER TABLE "token" ALTER COLUMN "access_token" TYPE VARCHAR(128) USING "access_token"::VARCHAR(128);""" diff --git a/api/asset_manager/src/mixins/__init__.py b/api/asset_manager/src/mixins/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/api/asset_manager/src/modules/__init__.py b/api/asset_manager/src/modules/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/api/asset_manager/src/modules/assets/__init__.py b/api/asset_manager/src/modules/assets/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/api/asset_manager/src/modules/auth/__init__.py b/api/asset_manager/src/modules/auth/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/api/asset_manager/src/modules/auth/router.py b/api/asset_manager/src/modules/auth/router.py index 39cf1c08..f1d80b56 100644 --- a/api/asset_manager/src/modules/auth/router.py +++ b/api/asset_manager/src/modules/auth/router.py @@ -31,7 +31,7 @@ async def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]): Logs the user into our API, creates tokens and passes them back to User. """ - user: User | None = await User.filter(email=form.username).first() + user: User | None = await User.filter(Q(email=form.username) | Q(username=form.username)).first() if user is None: raise HTTPException(status_code=401, detail=account_error) diff --git a/api/asset_manager/src/modules/organizations/__init__.py b/api/asset_manager/src/modules/organizations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/api/asset_manager/src/modules/users/__init__.py b/api/asset_manager/src/modules/users/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/api/asset_manager/src/requirements/requirements.txt b/api/asset_manager/src/requirements/requirements.txt index 4d6f979c..dd831c37 100644 --- a/api/asset_manager/src/requirements/requirements.txt +++ b/api/asset_manager/src/requirements/requirements.txt @@ -1,21 +1,22 @@ -aerich>=0.8.0 -fastapi[all]>=0.115.5 -python-dotenv>=0.21.0 -tortoise-orm[asyncpg]>=0.22.1 -uvicorn>=0.31.1 -black>=24.10.0 -joserfc>=1.0.1 +aerich>=0.9.0 +fastapi[all]>=0.115.12 +tortoise-orm[asyncpg]>=0.25.1 +uvicorn>=0.34.3 +black>=25.1.0 +joserfc>=1.1.0 passlib>=1.7.4 -pytz>=2024.2 -ptpython>=0.25 +pytz>=2025.2 +ptpython>=3.0.30 msgspec>=0.19.0 -bcrypt>=4.2.1 +bcrypt>=4.3.0 +tomlkit>=0.13.3 # Test Suite httpx>=0.28.1 -pytest>=8.3.4 -mock>=5.1.0 +mock>=5.2.0 +pytest>=8.4.0 asyncio>=3.4.3 -pytest-mock>=3.14.0 -pytest-asyncio>=0.25.3 -asgi-lifespan>=2.1.0 \ No newline at end of file +pytest-mock>=3.14.1 +pytest-asyncio>=1.0.0 +asgi-lifespan>=2.1.0 +Faker>=37.4.0 \ No newline at end of file diff --git a/api/asset_manager/src/tests/base_test.py b/api/asset_manager/src/tests/base_test.py new file mode 100644 index 00000000..2250157f --- /dev/null +++ b/api/asset_manager/src/tests/base_test.py @@ -0,0 +1,4 @@ + + +class Test(): + pass \ No newline at end of file diff --git a/api/asset_manager/src/tests/conftest.py b/api/asset_manager/src/tests/conftest.py index 8f2665c5..fa405308 100644 --- a/api/asset_manager/src/tests/conftest.py +++ b/api/asset_manager/src/tests/conftest.py @@ -1,19 +1,9 @@ -import asyncio +import asyncio, httpx, pytest from contextlib import asynccontextmanager from typing import AsyncGenerator -import httpx, pytest -from config import settings -from glob import glob +from asgi_lifespan import LifespanManager -from asgi_lifespan import LifespanManager # type: ignore - -settings.PSQL_DB_NAME = settings.PSQL_TEST_DB_NAME - -pytest_plugins = [ - fixture.replace("/", ".").replace("\\", ".").replace(".py", "") - for fixture in glob("tests/fixtures/*.py") - if "__" not in fixture -] +from tests.fixtures.account import * try: from main import app @@ -27,12 +17,12 @@ except ImportError: ClientManagerType = AsyncGenerator[httpx.AsyncClient, None] -@pytest.fixture(scope="session") +@pytest.fixture def anyio_backend(): return "asyncio" -@pytest.fixture(scope="session") +@pytest.fixture def event_loop(): loop = asyncio.get_event_loop() yield loop @@ -43,8 +33,9 @@ def event_loop(): async def client_manager(app, base_url="https://localhost", **kw) -> ClientManagerType: app.state.testing = True async with LifespanManager(app): - transport = httpx.ASGITransport(app=app) - async with httpx.AsyncClient(transport=transport, base_url=base_url, **kw) as c: + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=app), base_url=base_url, **kw + ) as c: yield c 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/account.py b/api/asset_manager/src/tests/fixtures/account.py new file mode 100644 index 00000000..d7a78b24 --- /dev/null +++ b/api/asset_manager/src/tests/fixtures/account.py @@ -0,0 +1,70 @@ + +import pytest +from dataclasses import dataclass +from modules.auth.utils import create_jwt_tokens +from modules.organizations.models import Organization, OrganizationType +from modules.users.models import ACL, Membership, User +from modules.auth.models import Token +from tortoise.expressions import Q + +from config import settings + +crypt = settings.CRYPT + +@dataclass +class user_creation_return_type: + user: User + organization: Organization + acl: ACL + tokens: Token + +@pytest.fixture() +async def create_user_with_org(): + async def inner_function(email="user@localhost.com", + username="user", + name="awesome", + surname="user", + password="password-dont-use", + organization_name="simple organization", + organization_type=OrganizationType.HOME, + is_admin=False) -> user_creation_return_type: + org: Organization | None = await Organization.filter(Q(name=organization_name) & Q(name=organization_type)).first() + if not org: + org: Organization = await Organization.create( + name=organization_name, + type=organization_type + ) + + user: User | None = await User.filter(Q(email=email)).first() + if not user: + user: User = await User.create( + email=email, + username=username, + name=name, + surname=surname, + password=crypt.hash(password), + ) + + acl: ACL | None = await ACL.filter(Q(id="5f33facd-08dd-48a1-8f15-3b24f2a727f5")).first() + if not acl: + acl: ACL = await ACL.create( + id="5f33facd-08dd-48a1-8f15-3b24f2a727f5", + READ=True, + WRITE=True, + REPORT=True, + MANAGE=True if is_admin else False, + ADMIN=True if is_admin else False, + ) + + membership: Membership | None = await Membership.filter(Q(user=user, organization=org, acl=acl)).first() + if not membership: + await Membership.get_or_create( + organization=org, + user=user, + acl=acl + ) + + tokens: Token = await create_jwt_tokens(user=user) + + return user, org, acl, tokens + return inner_function \ No newline at end of file diff --git a/api/asset_manager/src/tests/fixtures/account_fixtures.py b/api/asset_manager/src/tests/fixtures/account_fixtures.py deleted file mode 100644 index ac49fac0..00000000 --- a/api/asset_manager/src/tests/fixtures/account_fixtures.py +++ /dev/null @@ -1,86 +0,0 @@ -from modules.organizations.models import Organization, OrganizationType -from modules.users.models import ACL, Membership, User -import pytest # type=ignore -from config import settings - -crypt = settings.CRYPT - - -@pytest.fixture() -async def use_user_account(): - org, _ = await Organization.get_or_create( - id="6ad4c94e-0522-4912-8d16-02d451f4c92d", - defaults={ - "name": "User's Organization", - "type": OrganizationType.HOME, - }, - ) - acl, _ = await ACL.get_or_create( - id="a4e927a3-36e5-4761-badb-0a44ade6616f", - defaults={ - "READ": True, - "WRITE": True, - "REPORT": True, - "MANAGE": False, - "ADMIN": False, - }, - ) - user, _ = await User.get_or_create( - id="24235427-9662-4ba3-a9c5-00000000000b", - defaults={ - "email": "user@localhost.com", - "username": "user", - "name": "awesome", - "surname": "user", - "password": crypt.hash("userpassword"), - }, - ) - membership, _ = await Membership.get_or_create( - id="833b9511-b2da-4760-8fa4-1a5c7059911e", - defaults={ - "organization": org, - "user": user, - "acl": acl, - }, - ) - return org, acl, user, membership - - -@pytest.fixture() -async def use_admin_account(): - org, _ = await Organization.get_or_create( - id="de001f44-1bb8-4667-9f9d-2d62d6ad7270", - defaults={ - "name": "Admin's Organization", - "type": OrganizationType.EXTRA_LARGE_ORGANIZATION, - }, - ) - acl, _ = await ACL.get_or_create( - id="83c1bfe6-c2ed-4ba1-be03-0e5c1960ec31", - defaults={ - "READ": True, - "WRITE": True, - "REPORT": True, - "MANAGE": True, - "ADMIN": True, - }, - ) - user, _ = await User.get_or_create( - id="24235427-9662-4ba3-a9c5-00000000000a", - defaults={ - "email": "admin@localhost.com", - "username": "admin", - "name": "awesome", - "surname": "admin", - "password": crypt.hash("adminpassword"), - }, - ) - membership, _ = await Membership.get_or_create( - id="393473ee-c218-4bcf-82cd-cb676c4d8a33", - defaults={ - "organization": org, - "user": user, - "acl": acl, - }, - ) - return org, acl, user, membership diff --git a/api/asset_manager/src/tests/test_accounts/__init__.py b/api/asset_manager/src/tests/test_accounts/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/api/asset_manager/src/tests/test_authentication/__init__.py b/api/asset_manager/src/tests/test_authentication/__init__.py deleted file mode 100644 index e69de29b..00000000 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 5ed5092c..c8fe4756 100644 --- a/api/asset_manager/src/tests/test_authentication/test_authentication.py +++ b/api/asset_manager/src/tests/test_authentication/test_authentication.py @@ -1,3 +1,4 @@ +from tests.base_test import Test from modules.users.models import User import pytest # type: ignore from httpx import AsyncClient @@ -8,8 +9,7 @@ from tortoise.expressions import Q crypt = settings.CRYPT -class TestAuthentication(object): - @pytest.mark.asyncio +class TestAuthentication(Test): async def test_authentication_with_non_existing_user_and_password( self, client: AsyncClient ): @@ -24,11 +24,10 @@ class TestAuthentication(object): assert response.status_code == 401 assert response.json() == {"detail": "E-Mail Address or password is incorrect"} - @pytest.mark.asyncio async def test_authentication_with_existing_user_and_wrong_password( - self, client: AsyncClient, use_admin_account + self, client: AsyncClient, create_user_with_org ): - _, _, _, _ = use_admin_account + _, _, _, _ = await create_user_with_org() response = await client.post( "https://localhost/api/v1/auth/login", data={ @@ -40,11 +39,10 @@ class TestAuthentication(object): assert response.status_code == 401 assert response.json() == {"detail": "E-Mail Address or password is incorrect"} - @pytest.mark.asyncio async def test_authentication_with_existing_user_and_password( - self, client: AsyncClient, use_admin_account + self, client: AsyncClient, create_user_with_org ): - _, _, admin, _ = use_admin_account + admin, _, _, _ = await create_user_with_org(email="admin@localhost.com", password="adminpassword") response = await client.post( "https://localhost/api/v1/auth/login", data={ @@ -68,19 +66,19 @@ class TestAuthentication(object): } } - @pytest.mark.asyncio async def test_logging_out_destroys_tokens( - self, client: AsyncClient, use_user_account + self, client: AsyncClient, create_user_with_org ): - _, _, user, _ = use_user_account + user, _, _, _ = await create_user_with_org(email="superuser@localhost.com", password="superuser") response = await client.post( "https://localhost/api/v1/auth/login", data={ - "username": "user@localhost.com", - "password": "userpassword", + "username": "superuser@localhost.com", + "password": "superuser", "grant_type": "password", }, ) + print(response.json()) assert response.status_code == 200 assert response.json() == { "jwt": { @@ -115,11 +113,10 @@ class TestAuthentication(object): "detail": "Refresh token not found or something went wrong." } - @pytest.mark.asyncio async def test_create_new_tokens_upon_refresh( - self, client: AsyncClient, use_admin_account + self, client: AsyncClient, create_user_with_org ): - _, _, admin, _ = use_admin_account + admin, _, _, _ = await create_user_with_org(email="admin@localhost.com", password="adminpassword") token = await client.post( "https://localhost/api/v1/auth/login", data={ @@ -165,7 +162,6 @@ class TestAuthentication(object): } } - @pytest.mark.asyncio async def test_setup_new_account(self, client: AsyncClient): # Ensure account is never available. Prevents account already being available. check_if_account_exists: User | None = await User.filter( diff --git a/api/asset_manager/src/tests/test_general_routes/__init__.py b/api/asset_manager/src/tests/test_general_routes/__init__.py deleted file mode 100644 index e69de29b..00000000