theo-agent-dashboard

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

test_auth.py (4010B)


      1 """T013 — Tests for auth (password hashing + JWT)."""
      2 
      3 import time
      4 
      5 import pytest
      6 
      7 from app.auth import (
      8     create_jwt,
      9     hash_password,
     10     verify_jwt,
     11     verify_password,
     12 )
     13 
     14 
     15 class TestPasswordHashing:
     16     """argon2id password hashing."""
     17 
     18     def test_hash_password_returns_string(self) -> None:
     19         """Hash is a non-empty string."""
     20         hashed = hash_password("mysecret")
     21         assert isinstance(hashed, str)
     22         assert len(hashed) > 20
     23 
     24     def test_verify_password_correct(self) -> None:
     25         """Correct password verifies successfully."""
     26         hashed = hash_password("correct-horse")
     27         assert verify_password("correct-horse", hashed) is True
     28 
     29     def test_verify_password_wrong(self) -> None:
     30         """Wrong password returns False."""
     31         hashed = hash_password("correct-horse")
     32         assert verify_password("wrong-battery", hashed) is False
     33 
     34     def test_different_hashes_for_same_input(self) -> None:
     35         """Each hash is unique (salted)."""
     36         h1 = hash_password("same-pass")
     37         h2 = hash_password("same-pass")
     38         assert h1 != h2
     39 
     40 
     41 class TestJWT:
     42     """JWT creation and verification."""
     43 
     44     def test_create_jwt_returns_string(self) -> None:
     45         """Token is a non-empty string."""
     46         token = create_jwt({"sub": "user1"})
     47         assert isinstance(token, str)
     48         assert len(token) > 20
     49 
     50     def test_verify_jwt_decodes_valid_token(self) -> None:
     51         """Valid token decodes to original payload."""
     52         token = create_jwt({"sub": "user1"})
     53         payload = verify_jwt(token)
     54         assert payload["sub"] == "user1"
     55 
     56     def test_verify_jwt_raises_on_expired(self) -> None:
     57         """Expired token raises an exception."""
     58         token = create_jwt({"sub": "user1"}, expires_in_seconds=-1)
     59         with pytest.raises(Exception):
     60             verify_jwt(token)
     61 
     62     def test_verify_jwt_raises_on_invalid(self) -> None:
     63         """Tampered token raises an exception."""
     64         token = create_jwt({"sub": "user1"})
     65         with pytest.raises(Exception):
     66             verify_jwt(token + "tampered")
     67 
     68 
     69 class TestSecretManagement:
     70     """Secret key loading and creation."""
     71 
     72     def test_get_secret_from_env(self, monkeypatch) -> None:
     73         """_get_secret_key reads from THEO_JWT_SECRET env var."""
     74         monkeypatch.setenv("THEO_JWT_SECRET", "env-secret-123")
     75         from app.auth import _get_secret_key
     76         assert _get_secret_key() == "env-secret-123"
     77 
     78     def test_load_or_create_secret_creates_file(self, tmp_path, monkeypatch) -> None:
     79         """Creates .jwt_secret file when it doesn't exist."""
     80         monkeypatch.setenv("THEO_DATA_DIR", str(tmp_path))
     81         from app.auth import _load_or_create_secret
     82         secret = _load_or_create_secret()
     83         assert len(secret) == 64  # 32 bytes hex
     84         secret_file = tmp_path / ".jwt_secret"
     85         assert secret_file.exists()
     86         assert secret_file.read_text().strip() == secret
     87 
     88     def test_load_or_create_secret_reads_existing(self, tmp_path, monkeypatch) -> None:
     89         """Reads existing .jwt_secret file."""
     90         monkeypatch.setenv("THEO_DATA_DIR", str(tmp_path))
     91         (tmp_path / ".jwt_secret").write_text("persisted-secret")
     92         from app.auth import _load_or_create_secret
     93         assert _load_or_create_secret() == "persisted-secret"
     94 
     95     def test_create_jwt_uses_secret_key(self) -> None:
     96         """JWT is signed with the secret key."""
     97         token = create_jwt({"sub": "test"})
     98         payload = verify_jwt(token)
     99         assert payload["sub"] == "test"
    100         assert "exp" in payload
    101 
    102     def test_create_jwt_custom_expiry(self) -> None:
    103         """Custom expiry is encoded in the token."""
    104         token = create_jwt({"sub": "test"}, expires_in_seconds=3600)
    105         payload = verify_jwt(token)
    106         assert payload["exp"] > int(time.time())
    107 
    108     def test_verify_jwt_raises_on_malformed(self) -> None:
    109         """Non-JWT string raises."""
    110         with pytest.raises(Exception):
    111             verify_jwt("not-a-jwt-token")
    112