feat: add test framework, unit tests, and GitLab CI pipeline
- Add pytest config in backend/pyproject.toml with asyncio mode and coverage - Add backend/requirements-test.txt with pytest, pytest-cov, pytest-asyncio, aiosqlite - Write 86 backend unit tests covering auth (JWT tokens), schemas (Pydantic models), config (Settings env overrides), log_parser (log parsing, IP resolution, geo data), models (SQLAlchemy table definitions + column defaults), and theme export - Add Vitest config for Angular frontend (vitest.config.ts, src/test-setup.ts) - Add .gitlab-ci.yml with stages: backend-test, frontend-test, backend-lint, docker-build, and quality-gate - CI enforces 10% minimum coverage threshold for backend
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
"""Tests for app.config — Settings model and environment variable overrides."""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from app.config import Settings
|
||||
|
||||
|
||||
def _clear_kmn_env(monkeypatch):
|
||||
"""Remove all KMTN_ prefixed env vars so Settings reads pure defaults."""
|
||||
keys = [k for k in os.environ if k.startswith("KMTN_")]
|
||||
for k in keys:
|
||||
monkeypatch.delenv(k, raising=False)
|
||||
|
||||
|
||||
class TestSettingsDefaults:
|
||||
"""Verify Settings defaults match documented behavior."""
|
||||
|
||||
def test_default_database_url(self, monkeypatch):
|
||||
_clear_kmn_env(monkeypatch)
|
||||
settings = Settings()
|
||||
assert "postgresql" in settings.DATABASE_URL
|
||||
assert "kmountain" in settings.DATABASE_URL
|
||||
|
||||
def test_default_env_is_development(self, monkeypatch):
|
||||
_clear_kmn_env(monkeypatch)
|
||||
settings = Settings()
|
||||
assert settings.ENV == "development"
|
||||
|
||||
def test_default_jwt_expire_minutes(self, monkeypatch):
|
||||
_clear_kmn_env(monkeypatch)
|
||||
settings = Settings()
|
||||
assert settings.JWT_EXPIRE_MINUTES == 1440
|
||||
|
||||
def test_default_download_token_minutes(self, monkeypatch):
|
||||
_clear_kmn_env(monkeypatch)
|
||||
settings = Settings()
|
||||
assert settings.DOWNLOAD_TOKEN_MINUTES == 10
|
||||
|
||||
def test_default_cors_origins(self, monkeypatch):
|
||||
_clear_kmn_env(monkeypatch)
|
||||
settings = Settings()
|
||||
assert "http://localhost:4200" in settings.CORS_ORIGINS
|
||||
|
||||
def test_empty_admin_credentials_by_default(self, monkeypatch):
|
||||
_clear_kmn_env(monkeypatch)
|
||||
settings = Settings()
|
||||
assert settings.ADMIN_USERNAME == ""
|
||||
assert settings.ADMIN_PASSWORD == ""
|
||||
|
||||
|
||||
class TestSettingsEnvOverride:
|
||||
"""Verify KMTN_ prefixed env vars override defaults."""
|
||||
|
||||
def test_database_url_from_env(self, monkeypatch):
|
||||
monkeypatch.setenv("KMTN_DATABASE_URL", "sqlite+aiosqlite:///test.db")
|
||||
settings = Settings()
|
||||
assert settings.DATABASE_URL == "sqlite+aiosqlite:///test.db"
|
||||
|
||||
def test_env_from_env(self, monkeypatch):
|
||||
monkeypatch.setenv("KMTN_ENV", "production")
|
||||
settings = Settings()
|
||||
assert settings.ENV == "production"
|
||||
|
||||
def test_jwt_secret_from_env(self, monkeypatch):
|
||||
monkeypatch.setenv("KMTN_JWT_SECRET_KEY", "my-custom-secret")
|
||||
settings = Settings()
|
||||
assert settings.JWT_SECRET_KEY == "my-custom-secret"
|
||||
|
||||
def test_jwt_expire_override(self, monkeypatch):
|
||||
monkeypatch.setenv("KMTN_JWT_EXPIRE_MINUTES", "60")
|
||||
settings = Settings()
|
||||
assert settings.JWT_EXPIRE_MINUTES == 60
|
||||
|
||||
def test_logs_dir_override(self, monkeypatch):
|
||||
monkeypatch.setenv("KMTN_LOGS_DIR", "/custom/logs")
|
||||
settings = Settings()
|
||||
assert settings.LOGS_DIR == "/custom/logs"
|
||||
|
||||
def test_geolite2_path_override(self, monkeypatch):
|
||||
monkeypatch.setenv("KMTN_GEOLITE2_DB_PATH", "/custom/geo.mmdb")
|
||||
settings = Settings()
|
||||
assert settings.GEOLITE2_DB_PATH == "/custom/geo.mmdb"
|
||||
|
||||
def test_theme_json_path_override(self, monkeypatch):
|
||||
monkeypatch.setenv("KMTN_THEME_JSON_PATH", "/custom/theme.json")
|
||||
settings = Settings()
|
||||
assert settings.THEME_JSON_PATH == "/custom/theme.json"
|
||||
|
||||
def test_admin_credentials_from_env(self, monkeypatch):
|
||||
monkeypatch.setenv("KMTN_ADMIN_USERNAME", "admin")
|
||||
monkeypatch.setenv("KMTN_ADMIN_PASSWORD", "secure123")
|
||||
settings = Settings()
|
||||
assert settings.ADMIN_USERNAME == "admin"
|
||||
assert settings.ADMIN_PASSWORD == "secure123"
|
||||
|
||||
def test_website_url_override(self, monkeypatch):
|
||||
monkeypatch.setenv("KMTN_WEBSITE_URL", "https://example.com")
|
||||
settings = Settings()
|
||||
assert settings.WEBSITE_URL == "https://example.com"
|
||||
|
||||
def test_static_upstream_url_override(self, monkeypatch):
|
||||
monkeypatch.setenv("KMTN_STATIC_UPSTREAM_URL", "http://frontend:80")
|
||||
settings = Settings()
|
||||
assert settings.STATIC_UPSTREAM_URL == "http://frontend:80"
|
||||
|
||||
def test_cors_origins_override(self, monkeypatch):
|
||||
# pydantic-settings parses JSON lists for list[str] fields
|
||||
monkeypatch.setenv("KMTN_CORS_ORIGINS", '["https://example.com"]')
|
||||
settings = Settings()
|
||||
assert settings.CORS_ORIGINS == ["https://example.com"]
|
||||
|
||||
def test_kmn_prefix_is_required(self, monkeypatch):
|
||||
"""Non-prefixed env vars should NOT override settings."""
|
||||
monkeypatch.setenv("JWT_SECRET_KEY", "should-not-apply")
|
||||
settings = Settings()
|
||||
assert settings.JWT_SECRET_KEY != "should-not-apply"
|
||||
Reference in New Issue
Block a user