Files
intervalsicu-mcp-ui/tests/test_crypto.py
T
Chris Farhood bb5e9d518c
build-image / test (push) Successful in 37s
build-image / build (push) Successful in 25s
feat: Intervals.icu MCP portal (FastAPI + Authentik OIDC)
- OIDC login via Authentik; first login creates a disabled user (admin approval).
- Users set/replace their Intervals.icu athlete ID + API key; the key is validated
  against Intervals.icu and stored AES-256-GCM encrypted (shared key with the MCP
  server). Same users table (schema owned by the MCP server's migrations).
- Admin page (gated on the intervalsicu-mcp-admins group claim): list, approve,
  disable, delete users.
- 29 tests @ 93% (OIDC routes integration-only); Dockerfile asserts templates are
  packaged; .gitea CI test-gates the image build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 20:51:46 -04:00

75 lines
2.1 KiB
Python

"""
Tests for intervals_mcp_server.crypto (AES-256-GCM for the user API key).
Covers round-trip, per-message nonce randomness, tamper/wrong-key detection,
and key loading/validation from the environment.
"""
import base64
import pytest
from intervalsicu_mcp_ui import crypto
from intervalsicu_mcp_ui.crypto import CryptoError
KEY = crypto.load_key(crypto.generate_key_b64())
def test_roundtrip():
assert crypto.decrypt(crypto.encrypt("s3cr3t-api-key", KEY), KEY) == "s3cr3t-api-key"
def test_same_plaintext_encrypts_differently():
a = crypto.encrypt("same", KEY)
b = crypto.encrypt("same", KEY)
assert a != b # random nonce per message
assert crypto.decrypt(a, KEY) == crypto.decrypt(b, KEY) == "same"
def test_ciphertext_does_not_contain_plaintext():
assert b"api-key" not in crypto.encrypt("my-api-key", KEY)
def test_wrong_key_rejected():
blob = crypto.encrypt("x", KEY)
with pytest.raises(CryptoError):
crypto.decrypt(blob, crypto.load_key(crypto.generate_key_b64()))
def test_tampered_ciphertext_rejected():
blob = bytearray(crypto.encrypt("x", KEY))
blob[-1] ^= 0x01 # flip a bit in the GCM tag
with pytest.raises(CryptoError):
crypto.decrypt(bytes(blob), KEY)
def test_short_ciphertext_rejected():
with pytest.raises(CryptoError):
crypto.decrypt(b"tiny", KEY)
def test_load_key_from_env(monkeypatch):
monkeypatch.setenv("INTERVALS_ENC_KEY", crypto.generate_key_b64())
assert len(crypto.load_key()) == 32
def test_load_key_missing(monkeypatch):
monkeypatch.delenv("INTERVALS_ENC_KEY", raising=False)
with pytest.raises(CryptoError):
crypto.load_key()
def test_load_key_wrong_length():
with pytest.raises(CryptoError):
crypto.load_key(base64.b64encode(b"too-short").decode())
def test_load_key_bad_base64():
with pytest.raises(CryptoError):
crypto.load_key("!!!not-base64!!!")
def test_encrypt_defaults_to_env_key(monkeypatch):
monkeypatch.setenv("INTERVALS_ENC_KEY", crypto.generate_key_b64())
assert crypto.decrypt(crypto.encrypt("hello")) == "hello"