From 1b1142a0b071390400ad2cfabdf20ee8f4e00430 Mon Sep 17 00:00:00 2001 From: L-Nafaryus Date: Fri, 30 Aug 2024 12:38:43 +0500 Subject: [PATCH] repair tests --- src/materia/app/app.py | 139 +++++++++++++++---------------- src/materia/models/repository.py | 6 +- src/materia/routers/api/file.py | 5 +- tests/conftest.py | 69 ++++++++------- tests/test_api.py | 6 +- tests/test_models.py | 3 +- 6 files changed, 113 insertions(+), 115 deletions(-) diff --git a/src/materia/app/app.py b/src/materia/app/app.py index 7edc919..1e78f7f 100644 --- a/src/materia/app/app.py +++ b/src/materia/app/app.py @@ -29,123 +29,114 @@ class ApplicationError(Exception): class Application: - __instance__: Optional[Self] = None - def __init__( self, config: Config, - logger: LoggerInstance, - database: Database, - cache: Cache, - cron: Cron, - backend: FastAPI, ): - if Application.__instance__: - raise ApplicationError("Cannot create multiple applications") + self.config: Config = config + self.logger: Optional[LoggerInstance] = None + self.database: Optional[Database] = None + self.cache: Optional[Cache] = None + self.cron: Optional[Cron] = None + self.backend: Optional[FastAPI] = None - self.config = config - self.logger = logger - self.database = database - self.cache = cache - self.cron = cron - self.backend = backend + self.prepare_logger() @staticmethod async def new(config: Config): - if Application.__instance__: - raise ApplicationError("Cannot create multiple applications") - - logger = Logger.new(**config.log.model_dump()) + app = Application(config) # if user := config.application.user: # os.setuid(pwd.getpwnam(user).pw_uid) # if group := config.application.group: # os.setgid(pwd.getpwnam(user).pw_gid) - logger.debug("Initializing application...") + app.logger.debug("Initializing application...") + await app.prepare_working_directory() try: - logger.debug("Changing working directory") - os.chdir(config.application.working_directory.resolve()) - except FileNotFoundError as e: - logger.error("Failed to change working directory: {}", e) - sys.exit() - - try: - logger.info("Connecting to database {}", config.database.url()) - database = await Database.new(config.database.url()) # type: ignore - - logger.info("Connecting to cache server {}", config.cache.url()) - cache = await Cache.new(config.cache.url()) # type: ignore - - logger.info("Prepairing cron") - cron = Cron.new( - config.cron.workers_count, - backend_url=config.cache.url(), - broker_url=config.cache.url(), - ) - - logger.info("Running database migrations") - await database.run_migrations() + await app.prepare_database() + await app.prepare_cache() + await app.prepare_cron() + await app.prepare_server() except Exception as e: - logger.error(" ".join(e.args)) + app.logger.error(" ".join(e.args)) sys.exit() try: import materia_frontend except ModuleNotFoundError: - logger.warning( + app.logger.warning( "`materia_frontend` is not installed. No user interface will be served." ) + return app + + def prepare_logger(self): + self.logger = Logger.new(**self.config.log.model_dump()) + + async def prepare_working_directory(self): + try: + self.logger.debug("Changing working directory") + os.chdir(self.config.application.working_directory.resolve()) + except FileNotFoundError as e: + self.logger.error("Failed to change working directory: {}", e) + sys.exit() + + async def prepare_database(self): + url = self.config.database.url() + self.logger.info("Connecting to database {}", url) + self.database = await Database.new(url) # type: ignore + + async def prepare_cache(self): + url = self.config.cache.url() + self.logger.info("Connecting to cache server {}", url) + self.cache = await Cache.new(url) # type: ignore + + async def prepare_cron(self): + url = self.config.cache.url() + self.logger.info("Prepairing cron") + self.cron = Cron.new( + self.config.cron.workers_count, backend_url=url, broker_url=url + ) + + async def prepare_server(self): @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[Context]: - yield Context(config=config, logger=logger, database=database, cache=cache) + yield Context( + config=self.config, + logger=self.logger, + database=self.database, + cache=self.cache, + ) - if database.engine is not None: - await database.dispose() + if self.database.engine is not None: + await self.database.dispose() - backend = FastAPI( + self.backend = FastAPI( title="materia", version="0.1.0", docs_url="/api/docs", lifespan=lifespan, ) - backend.add_middleware( + self.backend.add_middleware( CORSMiddleware, allow_origins=["http://localhost", "http://localhost:5173"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) - backend.include_router(routers.api.router) - backend.include_router(routers.resources.router) - backend.include_router(routers.root.router) - - return Application( - config=config, - logger=logger, - database=database, - cache=cache, - cron=cron, - backend=backend, - ) - - @staticmethod - def instance() -> Optional[Self]: - return Application.__instance__ + self.backend.include_router(routers.api.router) + self.backend.include_router(routers.resources.router) + self.backend.include_router(routers.root.router) async def start(self): self.logger.info(f"Spinning up cron workers [{self.config.cron.workers_count}]") self.cron.run_workers() try: - # uvicorn.run( - # self.backend, - # port=self.config.server.port, - # host=str(self.config.server.address), - # # reload = config.application.mode == "development", - # log_config=Logger.uvicorn_config(self.config.log.level), - # ) + self.logger.info("Running database migrations") + await self.database.run_migrations() + uvicorn_config = uvicorn.Config( self.backend, port=self.config.server.port, @@ -157,3 +148,7 @@ class Application: await server.serve() except (KeyboardInterrupt, SystemExit): self.logger.info("Exiting...") + sys.exit() + except Exception as e: + self.logger.error(" ".join(e.args)) + sys.exit() diff --git a/src/materia/models/repository.py b/src/materia/models/repository.py index 4f0ea0c..d21bbc9 100644 --- a/src/materia/models/repository.py +++ b/src/materia/models/repository.py @@ -95,19 +95,19 @@ class Repository(Base): await session.refresh(user, attribute_names=["repository"]) return user.repository - async def used(self, session: SessionContext) -> int: + async def used_capacity(self, session: SessionContext) -> int: session.add(self) await session.refresh(self, attribute_names=["files"]) return sum([file.size for file in self.files]) async def remaining_capacity(self, session: SessionContext) -> int: - used = await self.used(session) + used = await self.used_capacity(session) return self.capacity - used async def info(self, session: SessionContext) -> "RepositoryInfo": info = RepositoryInfo.model_validate(self) - info.used = await self.used(session) + info.used = await self.used_capacity(session) return info diff --git a/src/materia/routers/api/file.py b/src/materia/routers/api/file.py index c212a63..e78e8de 100644 --- a/src/materia/routers/api/file.py +++ b/src/materia/routers/api/file.py @@ -74,8 +74,7 @@ async def create( repository: Repository = Depends(middleware.repository), ctx: middleware.Context = Depends(), ): - database = await Database.new(ctx.config.database.url(), test_connection=False) - async with database.session() as session: + async with ctx.database.session() as session: capacity = await repository.remaining_capacity(session) try: @@ -116,7 +115,7 @@ async def create( file.remove() raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Invalid path") - async with database.session() as session: + async with ctx.database.session() as session: target_directory = await validate_target_directory( path, repository, session, ctx.config ) diff --git a/tests/conftest.py b/tests/conftest.py index 0cc0daf..fb2ce93 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,27 +1,17 @@ import pytest_asyncio -from materia.config import Config from materia.models import ( - Database, - Cache, User, LoginType, ) from materia.models.base import Base from materia import security +from materia.app import Application +from materia.core import Config, Database, Cache, Cron import sqlalchemy as sa from sqlalchemy.pool import NullPool -from materia.app import make_application, AppContext -from materia._logging import make_logger from httpx import AsyncClient, ASGITransport, Cookies -import asyncio -from fastapi import FastAPI -from contextlib import asynccontextmanager -from typing import AsyncIterator from asgi_lifespan import LifespanManager -from fastapi.middleware.cors import CORSMiddleware -from materia import routers from pathlib import Path -from copy import deepcopy @pytest_asyncio.fixture(scope="session") @@ -75,6 +65,17 @@ async def cache(config: Config) -> Cache: yield cache_pytest +@pytest_asyncio.fixture(scope="session") +async def cron(config: Config) -> Cache: + cron_pytest = Cron.new( + config.cron.workers_count, + backend_url=config.cache.url(), + broker_url=config.cache.url(), + ) + + yield cron_pytest + + @pytest_asyncio.fixture(scope="function", autouse=True) async def setup_database(database: Database): async with database.connection() as connection: @@ -121,30 +122,36 @@ async def api_config(config: Config, tmpdir) -> Config: @pytest_asyncio.fixture(scope="function") async def api_client( - api_config: Config, database: Database, cache: Cache + api_config: Config, database: Database, cache: Cache, cron: Cron ) -> AsyncClient: - logger = make_logger(api_config) + app = Application(api_config) + app.database = database + app.cache = cache + app.cron = cron + await app.prepare_server() - @asynccontextmanager - async def lifespan(app: FastAPI) -> AsyncIterator[AppContext]: - yield AppContext( - config=api_config, database=database, cache=cache, logger=logger - ) + # logger = make_logger(api_config) - app = FastAPI(lifespan=lifespan) - app.include_router(routers.api.router) - app.include_router(routers.resources.router) - app.include_router(routers.root.router) - app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) + # @asynccontextmanager + # async def lifespan(app: FastAPI) -> AsyncIterator[AppContext]: + # yield AppContext( + # config=api_config, database=database, cache=cache, logger=logger + # ) - async with LifespanManager(app) as manager: + # app = FastAPI(lifespan=lifespan) + # app.include_router(routers.api.router) + # app.include_router(routers.resources.router) + # app.include_router(routers.root.router) + # app.add_middleware( + # CORSMiddleware, + # allow_origins=["*"], + # allow_credentials=True, + # allow_methods=["*"], + # allow_headers=["*"], + # ) + + async with LifespanManager(app.backend) as manager: async with AsyncClient( transport=ASGITransport(app=manager.app), base_url=api_config.server.url() ) as client: diff --git a/tests/test_api.py b/tests/test_api.py index 64e492a..5d9b123 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,8 +1,6 @@ import pytest -from materia.config import Config +from materia.core import Config from httpx import AsyncClient, Cookies -from materia.models.base import Base -import aiofiles from io import BytesIO # TODO: replace downloadable images for tests @@ -188,6 +186,6 @@ async def test_file(auth_client: AsyncClient, api_config: Config): pytest_logo = BytesIO(pytest_logo_res.content) create = await auth_client.post( - "/api/file", files={"file": ("pytest.png", pytest_logo)}, json={"path", "/"} + "/api/file", files={"file": ("pytest.png", pytest_logo)}, data={"path": "/"} ) assert create.status_code == 200, create.text diff --git a/tests/test_models.py b/tests/test_models.py index a9ea5bb..49506ff 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,6 @@ import pytest_asyncio import pytest from pathlib import Path -from materia.config import Config from materia.models import ( User, Repository, @@ -9,7 +8,7 @@ from materia.models import ( RepositoryError, File, ) -from materia.models.database import SessionContext +from materia.core import Config, SessionContext from materia import security import sqlalchemy as sa from sqlalchemy.orm.session import make_transient