Upgrade requirements, add new fixture and setup new testing procedure

This commit is contained in:
2025-06-23 12:37:22 +00:00
parent b1013659c4
commit 390152ac66
14 changed files with 177 additions and 230 deletions
+2 -1
View File
@@ -92,5 +92,6 @@
/web/**/thumb
/web/**/sketch
# Prevent uploading DB files
# Prevent some sensitive files from being committed
*.sqlite*
.env
+2 -2
View File
@@ -1,2 +1,2 @@
python 3.13.1
nodejs 23.4.0
python 3.13.5t
nodejs 24.2.0
+2 -1
View File
@@ -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 = ["*"]
+7 -2
View File
@@ -5,7 +5,6 @@ from aerich import Command
modules: dict[str, Any] = {
"models": [
"modules.assets.models",
"modules.auth.models",
"modules.users.models",
"modules.organizations.models",
@@ -14,6 +13,12 @@ modules: dict[str, Any] = {
TORTOISE_ORM = {
"connections": {
"testing": {
"engine": "tortoise.backends.sqlite",
"credentials": {
"file_path": "stoneedge.sqlite"
}
},
"default": {
"engine": "tortoise.backends.asyncpg",
"credentials": {
@@ -28,7 +33,7 @@ TORTOISE_ORM = {
"apps": {
"models": {
"models": modules.get("models", []) + ["aerich.models"],
"default_connection": "default",
"default_connection": "testing" if settings.IS_TESTING else "default",
},
},
}
+2 -1
View File
@@ -29,7 +29,8 @@ app = FastAPI(
default_response_class=msgspec_jsonresponse,
)
app.add_middleware(HTTPSRedirectMiddleware)
if settings.USE_HTTPS_ONLY:
app.add_middleware(HTTPSRedirectMiddleware)
app.add_middleware(TrustedHostMiddleware, allowed_hosts=[settings.PROJECT_PUBLIC_URL,])
# Set all CORS enabled origins
@@ -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 """
"""
@@ -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 """
"""
@@ -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";"""
@@ -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);"""
@@ -1,21 +1,21 @@
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
pytest-mock>=3.14.1
pytest-asyncio>=1.0.0
asgi-lifespan>=2.1.0
+3 -11
View File
@@ -2,18 +2,10 @@ import asyncio
from contextlib import asynccontextmanager
from typing import AsyncGenerator
import httpx, pytest
from config import settings
from glob import glob
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 +19,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
+60
View File
@@ -0,0 +1,60 @@
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 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 = await Organization.create(
name=organization_name,
type=organization_type
)
acl: ACL = await ACL.create(
READ=True,
WRITE=True,
REPORT=True,
MANAGE=True if is_admin else False,
ADMIN=True if is_admin else False,
)
user: User = await User.create(
email=email,
username=username,
name=name,
surname=surname,
password=crypt.hash(password),
)
await Membership.create(
organization=org,
user=user,
acl=acl
)
tokens: Token = await create_jwt_tokens(user=user)
return user, org, acl, tokens
return inner_function
@@ -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
@@ -26,9 +26,9 @@ class TestAuthentication(object):
@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={
@@ -42,9 +42,9 @@ class TestAuthentication(object):
@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={
@@ -70,9 +70,9 @@ 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="user@localhost.com", password="userpassword")
response = await client.post(
"https://localhost/api/v1/auth/login",
data={
@@ -117,9 +117,9 @@ class TestAuthentication(object):
@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={