diff --git a/alembic/env.py b/alembic/env.py index 9393056..8051754 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -6,7 +6,7 @@ from sqlalchemy import pool from alembic import context from foxnouns.db import Base -from foxnouns.settings import SYNC_DATABASE_URL +from foxnouns.db.sync import SYNC_DATABASE_URL # this is the Alembic Config object, which provides # access to the values within the .ini file in use. diff --git a/foxnouns/app.py b/foxnouns/app.py index 1ba7148..6729c51 100644 --- a/foxnouns/app.py +++ b/foxnouns/app.py @@ -1,4 +1,4 @@ -from quart import Quart, make_response, jsonify, request, g +from quart import Quart, request, g from quart_schema import QuartSchema, RequestSchemaValidationError from .blueprints import users_blueprint diff --git a/foxnouns/blueprints/v2/users.py b/foxnouns/blueprints/v2/users.py index 5451ac5..fdb5c56 100644 --- a/foxnouns/blueprints/v2/users.py +++ b/foxnouns/blueprints/v2/users.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, Field, field_validator -from quart import Blueprint, request +from quart import Blueprint from quart_schema import validate_response, validate_request from foxnouns.db.aio import async_session diff --git a/foxnouns/db/aio.py b/foxnouns/db/aio.py index 060b651..c43ae47 100644 --- a/foxnouns/db/aio.py +++ b/foxnouns/db/aio.py @@ -1,7 +1,7 @@ from sqlalchemy import URL from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker -from foxnouns.settings import DATABASE +from foxnouns.settings import DATABASE, ECHO_SQL ASYNC_DATABASE_URL = URL.create( "postgresql+asyncpg", @@ -11,5 +11,5 @@ ASYNC_DATABASE_URL = URL.create( database=DATABASE["NAME"], ) -engine = create_async_engine(ASYNC_DATABASE_URL) +engine = create_async_engine(ASYNC_DATABASE_URL, echo=ECHO_SQL) async_session = async_sessionmaker(engine, expire_on_commit=False) diff --git a/foxnouns/db/sync.py b/foxnouns/db/sync.py index c3e2006..97556ef 100644 --- a/foxnouns/db/sync.py +++ b/foxnouns/db/sync.py @@ -1,6 +1,6 @@ from sqlalchemy import URL, create_engine -from foxnouns.settings import DATABASE +from foxnouns.settings import DATABASE, ECHO_SQL SYNC_DATABASE_URL = URL.create( "postgresql+psycopg", @@ -10,4 +10,4 @@ SYNC_DATABASE_URL = URL.create( database=DATABASE["NAME"], ) -engine = create_engine(SYNC_DATABASE_URL) +engine = create_engine(SYNC_DATABASE_URL, echo=ECHO_SQL) diff --git a/foxnouns/models/user.py b/foxnouns/models/user.py index 79b8694..d640145 100644 --- a/foxnouns/models/user.py +++ b/foxnouns/models/user.py @@ -1,5 +1,3 @@ -import re - from pydantic import Field from . import BaseSnowflakeModel diff --git a/foxnouns/settings.py b/foxnouns/settings.py index 290b5da..0ac3c3b 100644 --- a/foxnouns/settings.py +++ b/foxnouns/settings.py @@ -20,3 +20,6 @@ SHORT_DOMAIN = env("SHORT_DOMAIN", "prns.localhost") # Secret key for signing tokens, generate with (for example) `openssl rand -base64 32` SECRET_KEY = env("SECRET_KEY") + +# Whether to echo SQL statements to the logs. +ECHO_SQL = env.bool("ECHO_SQL", False) diff --git a/tests/conftest.py b/tests/conftest.py index 1f6c3f2..1fd8593 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,61 @@ +import pytest import pytest_asyncio +from sqlalchemy import text, delete + +from foxnouns.db import Base +from foxnouns.settings import DATABASE + + +# Override the database name to the testing database +DATABASE["NAME"] = f"{DATABASE['NAME']}_test" + + +def pytest_collection_modifyitems(items): + """Ensure that all async tests use the same event loop.""" + + pytest_asyncio_tests = ( + item for item in items if pytest_asyncio.is_async_test(item) + ) + session_scope_marker = pytest.mark.asyncio(scope="session") + for async_test in pytest_asyncio_tests: + async_test.add_marker(session_scope_marker, append=False) + + +@pytest.fixture(scope="session", autouse=True) +def setup(): + """Migrate the testing database to the latest migration, and once the tests complete, clear the database again.""" + + from foxnouns.db.sync import engine + from alembic import command, config + + cfg = config.Config("alembic.ini") + cfg.attributes["connection"] = engine.connect() + command.upgrade(cfg, "head") + + yield + + with engine.begin() as session: + Base.metadata.drop_all(session) + session.execute(text("DROP TABLE alembic_version")) + session.commit() + + +@pytest.fixture(scope="function", autouse=True) +def clean_tables_after_tests(): + """Clean tables after every test.""" + + yield + + from foxnouns.db.sync import engine + + with engine.begin() as session: + for table in reversed(Base.metadata.sorted_tables): + session.execute(delete(table)) + session.commit() @pytest_asyncio.fixture(scope="session", autouse=True) -async def setup(): - print("hello from setup!") - yield - print("bye from setup!") +async def app(): + from foxnouns.app import app + + return app diff --git a/tests/test_hello.py b/tests/test_hello.py deleted file mode 100644 index 54f0f46..0000000 --- a/tests/test_hello.py +++ /dev/null @@ -1,8 +0,0 @@ -import pytest - -from foxnouns import hello - - -@pytest.mark.asyncio -async def test_hello(): - assert (await hello()) == "Hello world!" diff --git a/tests/test_users.py b/tests/test_users.py new file mode 100644 index 0000000..1d6f158 --- /dev/null +++ b/tests/test_users.py @@ -0,0 +1,13 @@ +import pytest +from quart import Quart + + +@pytest.mark.asyncio +class TestUsers: + async def test_get_me_returns_403_if_unauthenticated(self, app: Quart): + resp = await app.test_client().get("/api/v2/users/@me") + assert resp.status_code == 403 + + async def test_get_users_returns_404_if_user_not_found(self, app: Quart): + resp = await app.test_client().get("/api/v2/users/unknown_user") + assert resp.status_code == 404