- Add hash_password/verify_password to app/auth.py using bcrypt - Fix /login to reject bootstrap creds (403) when admin users exist - Add normal password-based login path for existing admin users - Eagerly load User.roles to avoid lazy-load outside async session - Fix test_user_models.py VARCHAR assertion (SQLAlchemy 2.0 uses 'string') - Use shared-cache SQLite in conftest for endpoint-level tests - Add 23 tests covering all 3 required scenarios plus edge cases: 1. Bootstrap login with hardcoded creds when no admins → PASS (200) 2. Bootstrap login with hardcoded creds when admins exist → FAIL (403) 3. Password login for existing admin with hashed password → PASS (200) - All 130 tests pass (107 existing + 23 new)
76 lines
3.0 KiB
Python
76 lines
3.0 KiB
Python
"""Shared test fixtures for kmtnflower backend tests."""
|
|
|
|
import os
|
|
import pytest
|
|
from unittest.mock import patch
|
|
|
|
# Set test environment vars BEFORE any app imports
|
|
# This is module-level so it runs when pytest loads this conftest
|
|
os.environ.setdefault("KMTN_DATABASE_URL", "sqlite+aiosqlite:///file::memory:?cache=shared")
|
|
os.environ.setdefault("KMTN_ENV", "test")
|
|
os.environ.setdefault("KMTN_JWT_SECRET_KEY", "test-secret-key-do-not-use")
|
|
os.environ.setdefault("KMTN_ADMIN_USERNAME", "testadmin")
|
|
os.environ.setdefault("KMTN_ADMIN_PASSWORD", "testpass123")
|
|
os.environ.setdefault("KMTN_LOGS_DIR", "/tmp/kmtn_test_logs")
|
|
os.environ.setdefault("KMTN_GEOLITE2_DB_PATH", "/nonexistent/GeoLite2-City.mmdb")
|
|
os.environ.setdefault("KMTN_THEME_JSON_PATH", "/tmp/kmtn_test_theme.json")
|
|
|
|
# Use shared-cache in-memory SQLite so all connections within the process
|
|
# see the same in-memory database (critical for tests that call the
|
|
# login endpoint which opens its own DB connections).
|
|
TEST_DB_URL = "sqlite+aiosqlite:///file::memory:?cache=shared"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def override_settings():
|
|
"""Set test-only environment variables before any import of app modules."""
|
|
env = {
|
|
"KMTN_DATABASE_URL": TEST_DB_URL,
|
|
"KMTN_ENV": "test",
|
|
"KMTN_JWT_SECRET_KEY": "test-secret-key-do-not-use",
|
|
"KMTN_ADMIN_USERNAME": "testadmin",
|
|
"KMTN_ADMIN_PASSWORD": "testpass123",
|
|
"KMTN_LOGS_DIR": "/tmp/kmtn_test_logs",
|
|
"KMTN_GEOLITE2_DB_PATH": "/nonexistent/GeoLite2-City.mmdb",
|
|
"KMTN_THEME_JSON_PATH": "/tmp/kmtn_test_theme.json",
|
|
}
|
|
with patch.dict("os.environ", env, clear=False):
|
|
yield env
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
async def reconfigure_database():
|
|
"""Reconfigure the SQLAlchemy engine to use shared in-memory SQLite.
|
|
|
|
Uses file::memory:?cache=shared so all connections within the process
|
|
see the same in-memory database (critical for tests that call the
|
|
login endpoint which opens its own DB connections).
|
|
|
|
Drops all tables at the START of each test to ensure isolation.
|
|
"""
|
|
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
|
|
|
from app import database
|
|
from app.models import Base as ModelsBase
|
|
|
|
# Create a new SQLite async engine with shared cache
|
|
test_engine = create_async_engine(TEST_DB_URL, echo=False)
|
|
test_session = async_sessionmaker(
|
|
test_engine, class_=database.AsyncSession, expire_on_commit=False
|
|
)
|
|
|
|
# Replace the module-level engine and session factory
|
|
database.engine = test_engine
|
|
database.async_session = test_session
|
|
|
|
# Drop all tables to ensure test isolation (shared-cache persists across tests)
|
|
# Then recreate them so fixture-dependent tests have a schema to work with.
|
|
async with test_engine.begin() as conn:
|
|
await conn.run_sync(ModelsBase.metadata.drop_all)
|
|
await conn.run_sync(ModelsBase.metadata.create_all)
|
|
|
|
yield test_engine
|
|
|
|
# Dispose of the test engine after each test
|
|
await test_engine.dispose()
|