mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-27 07:41:58 -06:00
Implement operator login service and fix static root
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from Data.Engine.config.environment import load_environment
|
||||
|
||||
|
||||
@@ -59,3 +61,14 @@ def test_static_root_falls_back_to_legacy_source(tmp_path, monkeypatch):
|
||||
assert settings.flask.static_root == legacy_source.resolve()
|
||||
|
||||
monkeypatch.delenv("BOREALIS_ROOT", raising=False)
|
||||
|
||||
|
||||
def test_resolve_project_root_defaults_to_repository(monkeypatch):
|
||||
"""The project root should resolve to the repository checkout."""
|
||||
|
||||
monkeypatch.delenv("BOREALIS_ROOT", raising=False)
|
||||
from Data.Engine.config import environment as env_module
|
||||
|
||||
expected = Path(env_module.__file__).resolve().parents[3]
|
||||
|
||||
assert env_module._resolve_project_root() == expected
|
||||
|
||||
63
Data/Engine/tests/test_operator_auth_builders.py
Normal file
63
Data/Engine/tests/test_operator_auth_builders.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Tests for operator authentication builders."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from Data.Engine.builders import (
|
||||
OperatorLoginRequest,
|
||||
OperatorMFAVerificationRequest,
|
||||
build_login_request,
|
||||
build_mfa_request,
|
||||
)
|
||||
|
||||
|
||||
def test_build_login_request_uses_explicit_hash():
|
||||
payload = {"username": "Admin", "password_sha512": "abc123"}
|
||||
|
||||
result = build_login_request(payload)
|
||||
|
||||
assert isinstance(result, OperatorLoginRequest)
|
||||
assert result.username == "Admin"
|
||||
assert result.password_sha512 == "abc123"
|
||||
|
||||
|
||||
def test_build_login_request_hashes_plain_password():
|
||||
payload = {"username": "user", "password": "secret"}
|
||||
|
||||
result = build_login_request(payload)
|
||||
|
||||
assert isinstance(result, OperatorLoginRequest)
|
||||
assert result.username == "user"
|
||||
assert result.password_sha512
|
||||
assert result.password_sha512 != "secret"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
{"password": "secret"},
|
||||
{"username": ""},
|
||||
{"username": "user"},
|
||||
],
|
||||
)
|
||||
def test_build_login_request_validation(payload):
|
||||
with pytest.raises(ValueError):
|
||||
build_login_request(payload)
|
||||
|
||||
|
||||
def test_build_mfa_request_normalizes_code():
|
||||
payload = {"pending_token": "token", "code": "12 34-56"}
|
||||
|
||||
result = build_mfa_request(payload)
|
||||
|
||||
assert isinstance(result, OperatorMFAVerificationRequest)
|
||||
assert result.pending_token == "token"
|
||||
assert result.code == "123456"
|
||||
|
||||
|
||||
def test_build_mfa_request_requires_token_and_code():
|
||||
with pytest.raises(ValueError):
|
||||
build_mfa_request({"code": "123"})
|
||||
with pytest.raises(ValueError):
|
||||
build_mfa_request({"pending_token": "token", "code": "12"})
|
||||
197
Data/Engine/tests/test_operator_auth_service.py
Normal file
197
Data/Engine/tests/test_operator_auth_service.py
Normal file
@@ -0,0 +1,197 @@
|
||||
"""Tests for the operator authentication service."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import pytest
|
||||
|
||||
pyotp = pytest.importorskip("pyotp")
|
||||
|
||||
from Data.Engine.builders import (
|
||||
OperatorLoginRequest,
|
||||
OperatorMFAVerificationRequest,
|
||||
)
|
||||
from Data.Engine.repositories.sqlite.connection import connection_factory
|
||||
from Data.Engine.repositories.sqlite.user_repository import SQLiteUserRepository
|
||||
from Data.Engine.services.auth.operator_auth_service import (
|
||||
InvalidCredentialsError,
|
||||
InvalidMFACodeError,
|
||||
OperatorAuthService,
|
||||
)
|
||||
|
||||
|
||||
def _prepare_db(path: Path) -> Callable[[], sqlite3.Connection]:
|
||||
conn = sqlite3.connect(path)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT,
|
||||
display_name TEXT,
|
||||
password_sha512 TEXT,
|
||||
role TEXT,
|
||||
last_login INTEGER,
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER,
|
||||
mfa_enabled INTEGER,
|
||||
mfa_secret TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return connection_factory(path)
|
||||
|
||||
|
||||
def _insert_user(
|
||||
factory: Callable[[], sqlite3.Connection],
|
||||
*,
|
||||
user_id: str,
|
||||
username: str,
|
||||
password_hash: str,
|
||||
role: str = "Admin",
|
||||
mfa_enabled: int = 0,
|
||||
mfa_secret: str = "",
|
||||
) -> None:
|
||||
conn = factory()
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO users (
|
||||
id, username, display_name, password_sha512, role,
|
||||
last_login, created_at, updated_at, mfa_enabled, mfa_secret
|
||||
) VALUES (?, ?, ?, ?, ?, 0, 0, 0, ?, ?)
|
||||
""",
|
||||
(user_id, username, username, password_hash, role, mfa_enabled, mfa_secret),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_authenticate_success_updates_last_login(tmp_path):
|
||||
db_path = tmp_path / "auth.db"
|
||||
factory = _prepare_db(db_path)
|
||||
password_hash = hashlib.sha512(b"password").hexdigest()
|
||||
_insert_user(factory, user_id="1", username="admin", password_hash=password_hash)
|
||||
|
||||
repo = SQLiteUserRepository(factory)
|
||||
service = OperatorAuthService(repo)
|
||||
|
||||
request = OperatorLoginRequest(username="admin", password_sha512=password_hash)
|
||||
result = service.authenticate(request)
|
||||
|
||||
assert result.username == "admin"
|
||||
|
||||
conn = factory()
|
||||
row = conn.execute("SELECT last_login FROM users WHERE username=?", ("admin",)).fetchone()
|
||||
conn.close()
|
||||
assert row[0] > 0
|
||||
|
||||
|
||||
def test_authenticate_invalid_credentials(tmp_path):
|
||||
db_path = tmp_path / "auth.db"
|
||||
factory = _prepare_db(db_path)
|
||||
repo = SQLiteUserRepository(factory)
|
||||
service = OperatorAuthService(repo)
|
||||
|
||||
request = OperatorLoginRequest(username="missing", password_sha512="abc")
|
||||
with pytest.raises(InvalidCredentialsError):
|
||||
service.authenticate(request)
|
||||
|
||||
|
||||
def test_mfa_verify_flow(tmp_path):
|
||||
db_path = tmp_path / "auth.db"
|
||||
factory = _prepare_db(db_path)
|
||||
secret = pyotp.random_base32()
|
||||
password_hash = hashlib.sha512(b"password").hexdigest()
|
||||
_insert_user(
|
||||
factory,
|
||||
user_id="1",
|
||||
username="admin",
|
||||
password_hash=password_hash,
|
||||
mfa_enabled=1,
|
||||
mfa_secret=secret,
|
||||
)
|
||||
|
||||
repo = SQLiteUserRepository(factory)
|
||||
service = OperatorAuthService(repo)
|
||||
login_request = OperatorLoginRequest(username="admin", password_sha512=password_hash)
|
||||
|
||||
challenge = service.authenticate(login_request)
|
||||
assert challenge.stage == "verify"
|
||||
|
||||
totp = pyotp.TOTP(secret)
|
||||
verify_request = OperatorMFAVerificationRequest(
|
||||
pending_token=challenge.pending_token,
|
||||
code=totp.now(),
|
||||
)
|
||||
|
||||
result = service.verify_mfa(challenge, verify_request)
|
||||
assert result.username == "admin"
|
||||
|
||||
|
||||
def test_mfa_setup_flow_persists_secret(tmp_path):
|
||||
db_path = tmp_path / "auth.db"
|
||||
factory = _prepare_db(db_path)
|
||||
password_hash = hashlib.sha512(b"password").hexdigest()
|
||||
_insert_user(
|
||||
factory,
|
||||
user_id="1",
|
||||
username="admin",
|
||||
password_hash=password_hash,
|
||||
mfa_enabled=1,
|
||||
mfa_secret="",
|
||||
)
|
||||
|
||||
repo = SQLiteUserRepository(factory)
|
||||
service = OperatorAuthService(repo)
|
||||
|
||||
challenge = service.authenticate(OperatorLoginRequest(username="admin", password_sha512=password_hash))
|
||||
assert challenge.stage == "setup"
|
||||
assert challenge.secret
|
||||
|
||||
totp = pyotp.TOTP(challenge.secret)
|
||||
verify_request = OperatorMFAVerificationRequest(
|
||||
pending_token=challenge.pending_token,
|
||||
code=totp.now(),
|
||||
)
|
||||
|
||||
result = service.verify_mfa(challenge, verify_request)
|
||||
assert result.username == "admin"
|
||||
|
||||
conn = factory()
|
||||
stored_secret = conn.execute(
|
||||
"SELECT mfa_secret FROM users WHERE username=?", ("admin",)
|
||||
).fetchone()[0]
|
||||
conn.close()
|
||||
assert stored_secret
|
||||
|
||||
|
||||
def test_mfa_invalid_code_raises(tmp_path):
|
||||
db_path = tmp_path / "auth.db"
|
||||
factory = _prepare_db(db_path)
|
||||
secret = pyotp.random_base32()
|
||||
password_hash = hashlib.sha512(b"password").hexdigest()
|
||||
_insert_user(
|
||||
factory,
|
||||
user_id="1",
|
||||
username="admin",
|
||||
password_hash=password_hash,
|
||||
mfa_enabled=1,
|
||||
mfa_secret=secret,
|
||||
)
|
||||
|
||||
repo = SQLiteUserRepository(factory)
|
||||
service = OperatorAuthService(repo)
|
||||
challenge = service.authenticate(OperatorLoginRequest(username="admin", password_sha512=password_hash))
|
||||
|
||||
verify_request = OperatorMFAVerificationRequest(
|
||||
pending_token=challenge.pending_token,
|
||||
code="000000",
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidMFACodeError):
|
||||
service.verify_mfa(challenge, verify_request)
|
||||
Reference in New Issue
Block a user