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:
Hermes Agent
2026-07-29 08:34:42 +00:00
parent 8d233b17a5
commit 501df79e5c
12 changed files with 887 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
"""Tests for app.auth — JWT token creation, verification, and decoding."""
import pytest
from datetime import datetime, timedelta, timezone
from fastapi import HTTPException
from jose import jwt
from app.auth import (
create_access_token,
create_download_token,
verify_download_token,
decode_token,
)
from app.config import settings
class TestCreateAccessToken:
"""Tests for create_access_token()."""
def test_returns_string_token(self):
token = create_access_token(user_id=1, is_admin=True)
assert isinstance(token, str)
assert len(token) > 0
def test_token_contains_correct_payload(self):
token = create_access_token(user_id=42, is_admin=False)
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=["HS256"])
assert payload["sub"] == "42"
assert payload["is_admin"] is False
assert "exp" in payload
def test_admin_flag_is_preserved(self):
admin_token = create_access_token(user_id=1, is_admin=True)
payload = jwt.decode(admin_token, settings.JWT_SECRET_KEY, algorithms=["HS256"])
assert payload["is_admin"] is True
def test_token_expires_in_configured_minutes(self):
token = create_access_token(user_id=1, is_admin=True)
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=["HS256"])
expected_exp = datetime.now(timezone.utc) + timedelta(minutes=settings.JWT_EXPIRE_MINUTES)
# Allow 1s drift
assert abs(payload["exp"] - expected_exp.timestamp()) < 2
class TestCreateDownloadToken:
"""Tests for create_download_token()."""
def test_download_token_contains_artifact_id(self):
token = create_download_token(artifact_id=99)
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=["HS256"])
assert payload["sub"] == "download"
assert payload["artifact_id"] == 99
def test_download_token_expires_quickly(self):
token = create_download_token(artifact_id=1)
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=["HS256"])
expected_exp = datetime.now(timezone.utc) + timedelta(minutes=settings.DOWNLOAD_TOKEN_MINUTES)
assert abs(payload["exp"] - expected_exp.timestamp()) < 2
class TestVerifyDownloadToken:
"""Tests for verify_download_token()."""
def test_valid_download_token_returns_artifact_id(self):
token = create_download_token(artifact_id=55)
result = verify_download_token(token)
assert result == 55
def test_access_token_rejected_as_download_token(self):
token = create_access_token(user_id=1, is_admin=True)
result = verify_download_token(token)
assert result is None
def test_expired_token_returns_none(self):
payload = {
"sub": "download",
"artifact_id": 1,
"exp": datetime.now(timezone.utc) - timedelta(minutes=1),
}
expired_token = jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm="HS256")
result = verify_download_token(expired_token)
assert result is None
def test_tampered_token_returns_none(self):
token = create_download_token(artifact_id=10)
tampered = token[:-5] + "xxxxx"
result = verify_download_token(tampered)
assert result is None
def test_wrong_secret_returns_none(self):
payload = {
"sub": "download",
"artifact_id": 1,
"exp": datetime.now(timezone.utc) + timedelta(minutes=5),
}
wrong_token = jwt.encode(payload, "wrong-secret", algorithm="HS256")
result = verify_download_token(wrong_token)
assert result is None
def test_non_integer_artifact_id_returns_none(self):
payload = {
"sub": "download",
"artifact_id": "not_an_int",
"exp": datetime.now(timezone.utc) + timedelta(minutes=5),
}
token = jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm="HS256")
result = verify_download_token(token)
assert result is None
class TestDecodeToken:
"""Tests for decode_token()."""
def test_valid_token_returns_payload(self):
token = create_access_token(user_id=7, is_admin=False)
payload = decode_token(token)
assert payload["sub"] == "7"
def test_expired_token_raises_401(self):
payload = {
"sub": "1",
"exp": datetime.now(timezone.utc) - timedelta(hours=1),
}
expired = jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm="HS256")
with pytest.raises(HTTPException) as exc_info:
decode_token(expired)
assert exc_info.value.status_code == 401
def test_invalid_signature_raises_401(self):
with pytest.raises(HTTPException) as exc_info:
decode_token("not.a.token")
assert exc_info.value.status_code == 401
def test_token_without_sub_raises_401(self):
payload = {
"exp": datetime.now(timezone.utc) + timedelta(hours=1),
}
token = jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm="HS256")
with pytest.raises(HTTPException) as exc_info:
decode_token(token)
assert exc_info.value.status_code == 401