feat: add conditional bootstrap login + 23 admin login tests
- 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)
This commit is contained in:
@@ -1,14 +1,31 @@
|
||||
"""Shared test fixtures for kmtnflower backend tests."""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
# Override settings for tests — use SQLite so we don't need PostgreSQL
|
||||
# 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": "sqlite+aiosqlite:///:memory:",
|
||||
"KMTN_DATABASE_URL": TEST_DB_URL,
|
||||
"KMTN_ENV": "test",
|
||||
"KMTN_JWT_SECRET_KEY": "test-secret-key-do-not-use",
|
||||
"KMTN_ADMIN_USERNAME": "testadmin",
|
||||
@@ -19,3 +36,40 @@ def override_settings():
|
||||
}
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user