Complete Python/NiceGUI rewrite of the Wirezone (Elixir/Phoenix) VPN management platform. All 10 implementation phases delivered. Core stack: - NiceGUI reactive UI with SQLModel ORM on PostgreSQL (asyncpg) - Alembic migrations, Valkey/Redis cache, pydantic-settings config - WireGuard management via subprocess (wg/ip/nft CLIs) - 164 tests passing, 35% code coverage Features: - User/device/rule CRUD with admin and unprivileged roles - Full device config form with per-device WG overrides - WireGuard client config generation with QR codes - REST API (v0) with Bearer token auth for all resources - TOTP MFA with QR registration and challenge flow - OIDC SSO with authlib (provider registry, auto-create users) - Magic link passwordless sign-in via email - SAML SP-initiated SSO with IdP metadata parsing - WebAuthn/FIDO2 security key registration - nftables firewall with per-user chains and masquerade - Background tasks: WG stats polling, VPN session expiry, OIDC token refresh, WAN connectivity checks - Startup reconciliation (DB ↔ WireGuard state sync) - In-memory notification system with header badge - Admin UI: users, devices, rules, settings (3 tabs), diagnostics - Loguru logging with optional timestamped file output Deployment: - Multi-stage Dockerfile (python:3.13-slim) - Docker Compose prod stack (bridge networking, NET_ADMIN, nftables) - Forgejo CI: tests → semantic versioning → Docker registry push - Health endpoint at /api/health
98 lines
2.8 KiB
Python
98 lines
2.8 KiB
Python
"""Tests for authentication modules."""
|
|
|
|
from sqlmodel import select
|
|
|
|
from wiregui.auth.jwt import create_access_token, decode_access_token
|
|
from wiregui.auth.passwords import hash_password, verify_password
|
|
from wiregui.auth.seed import seed_admin
|
|
from wiregui.models.user import User
|
|
|
|
|
|
# --- Password hashing ---
|
|
|
|
|
|
def test_hash_and_verify():
|
|
hashed = hash_password("my-secret")
|
|
assert verify_password("my-secret", hashed) is True
|
|
|
|
|
|
def test_verify_wrong_password():
|
|
hashed = hash_password("correct")
|
|
assert verify_password("wrong", hashed) is False
|
|
|
|
|
|
def test_hash_is_not_plaintext():
|
|
hashed = hash_password("plaintext")
|
|
assert hashed != "plaintext"
|
|
assert hashed.startswith("$2b$")
|
|
|
|
|
|
# --- JWT ---
|
|
|
|
|
|
def test_create_and_decode_token():
|
|
token = create_access_token(user_id="user-123", role="admin")
|
|
payload = decode_access_token(token)
|
|
assert payload is not None
|
|
assert payload["sub"] == "user-123"
|
|
assert payload["role"] == "admin"
|
|
assert "exp" in payload
|
|
|
|
|
|
def test_decode_invalid_token():
|
|
assert decode_access_token("garbage.token.value") is None
|
|
|
|
|
|
def test_decode_tampered_token():
|
|
token = create_access_token(user_id="user-123", role="admin")
|
|
tampered = token[:-4] + "XXXX"
|
|
assert decode_access_token(tampered) is None
|
|
|
|
|
|
# --- Admin seed ---
|
|
|
|
|
|
async def test_seed_admin_creates_user(session, monkeypatch):
|
|
"""seed_admin should create an admin when no users exist."""
|
|
# Patch async_session to use our test session
|
|
from unittest.mock import AsyncMock
|
|
from contextlib import asynccontextmanager
|
|
|
|
@asynccontextmanager
|
|
async def mock_session():
|
|
yield session
|
|
|
|
monkeypatch.setattr("wiregui.auth.seed.async_session", mock_session)
|
|
monkeypatch.setattr("wiregui.auth.seed.get_settings", lambda: type("S", (), {
|
|
"admin_email": "seed-test@example.com",
|
|
"admin_password": "seed-pass-123",
|
|
})())
|
|
|
|
await seed_admin()
|
|
|
|
result = await session.execute(select(User).where(User.email == "seed-test@example.com"))
|
|
admin = result.scalar_one()
|
|
assert admin.role == "admin"
|
|
assert verify_password("seed-pass-123", admin.password_hash)
|
|
|
|
|
|
async def test_seed_admin_skips_when_users_exist(session, monkeypatch):
|
|
"""seed_admin should not create a second admin if users already exist."""
|
|
from contextlib import asynccontextmanager
|
|
|
|
existing = User(email="existing@example.com", role="unprivileged")
|
|
session.add(existing)
|
|
await session.flush()
|
|
|
|
@asynccontextmanager
|
|
async def mock_session():
|
|
yield session
|
|
|
|
monkeypatch.setattr("wiregui.auth.seed.async_session", mock_session)
|
|
|
|
await seed_admin()
|
|
|
|
result = await session.execute(select(User))
|
|
users = result.scalars().all()
|
|
assert len(users) == 1
|
|
assert users[0].email == "existing@example.com"
|