119 lines
5.2 KiB
Python
119 lines
5.2 KiB
Python
"""Tests for src/config.py — token validation logic (T-14)."""
|
|
|
|
import logging
|
|
import time
|
|
|
|
import jwt
|
|
import pytest
|
|
|
|
from src.config import _validate_chatgpt_token
|
|
|
|
|
|
class TestValidateChatGPTToken:
|
|
def test_expired_token_logs_warning(self, caplog):
|
|
# T-14: expired JWT must produce a clear warning
|
|
payload = {"exp": int(time.time()) - 3600} # expired 1 hour ago
|
|
token = jwt.encode(payload, "secret", algorithm="HS256")
|
|
with caplog.at_level(logging.WARNING, logger="src.config"):
|
|
result = _validate_chatgpt_token(token)
|
|
assert any("expired" in r.message.lower() for r in caplog.records)
|
|
assert result is not None # still returns the expiry datetime
|
|
|
|
def test_expiring_within_24h_logs_warning(self, caplog):
|
|
payload = {"exp": int(time.time()) + 3600} # expires in 1 hour
|
|
token = jwt.encode(payload, "secret", algorithm="HS256")
|
|
with caplog.at_level(logging.WARNING, logger="src.config"):
|
|
_validate_chatgpt_token(token)
|
|
assert any("less than 24 hours" in r.message for r in caplog.records)
|
|
|
|
def test_valid_token_no_expiry_warning(self, caplog):
|
|
payload = {"exp": int(time.time()) + 86400 * 5} # valid for 5 days
|
|
token = jwt.encode(payload, "secret", algorithm="HS256")
|
|
with caplog.at_level(logging.WARNING, logger="src.config"):
|
|
result = _validate_chatgpt_token(token)
|
|
assert not any("expired" in r.message.lower() for r in caplog.records)
|
|
assert result is not None
|
|
|
|
def test_token_without_exp_claim_logs_warning(self, caplog):
|
|
payload = {"sub": "user123"} # no exp
|
|
token = jwt.encode(payload, "secret", algorithm="HS256")
|
|
with caplog.at_level(logging.WARNING, logger="src.config"):
|
|
result = _validate_chatgpt_token(token)
|
|
assert any("'exp'" in r.message or "no 'exp'" in r.message for r in caplog.records)
|
|
assert result is None
|
|
|
|
def test_jwe_encrypted_token_returns_none(self, caplog):
|
|
# JWE tokens (alg=dir) cannot be decoded client-side — this is normal for ChatGPT
|
|
jwe_like = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0.fake.token.data.here"
|
|
with caplog.at_level(logging.DEBUG, logger="src.config"):
|
|
result = _validate_chatgpt_token(jwe_like)
|
|
assert result is None # cannot decode, but not an error
|
|
|
|
def test_non_jwt_string_logs_warning(self, caplog):
|
|
with caplog.at_level(logging.WARNING, logger="src.config"):
|
|
result = _validate_chatgpt_token("notajwttoken")
|
|
assert any("does not look like a JWT" in r.message for r in caplog.records)
|
|
assert result is None
|
|
|
|
|
|
class TestSessionLimiterConfig:
|
|
"""MAX_CONVERSATIONS_PER_RUN and REQUEST_DELAY parsing in load_config."""
|
|
|
|
def _load(self, monkeypatch, tmp_path, **env):
|
|
from src.config import load_config
|
|
monkeypatch.setenv("EXPORT_DIR", str(tmp_path / "exports"))
|
|
monkeypatch.setenv("CACHE_DIR", str(tmp_path / "cache"))
|
|
for key in (
|
|
"MAX_CONVERSATIONS_PER_RUN",
|
|
"REQUEST_DELAY",
|
|
"EXPORTER_HIDDEN_CONTENT",
|
|
"EXPORTER_DOWNLOAD_MEDIA",
|
|
):
|
|
monkeypatch.delenv(key, raising=False)
|
|
for key, value in env.items():
|
|
monkeypatch.setenv(key, value)
|
|
return load_config()
|
|
|
|
def test_defaults(self, monkeypatch, tmp_path):
|
|
cfg = self._load(monkeypatch, tmp_path)
|
|
assert cfg.max_conversations is None
|
|
assert cfg.request_delay == 1.0
|
|
assert cfg.hidden_content == "placeholder"
|
|
assert cfg.download_media == "images"
|
|
|
|
def test_download_media_valid(self, monkeypatch, tmp_path):
|
|
cfg = self._load(monkeypatch, tmp_path, EXPORTER_DOWNLOAD_MEDIA="all")
|
|
assert cfg.download_media == "all"
|
|
|
|
def test_download_media_invalid_raises(self, monkeypatch, tmp_path):
|
|
from src.config import ConfigError
|
|
with pytest.raises(ConfigError, match="EXPORTER_DOWNLOAD_MEDIA"):
|
|
self._load(monkeypatch, tmp_path, EXPORTER_DOWNLOAD_MEDIA="sometimes")
|
|
|
|
def test_valid_values(self, monkeypatch, tmp_path):
|
|
cfg = self._load(
|
|
monkeypatch, tmp_path,
|
|
MAX_CONVERSATIONS_PER_RUN="25", REQUEST_DELAY="0.5",
|
|
)
|
|
assert cfg.max_conversations == 25
|
|
assert cfg.request_delay == 0.5
|
|
|
|
def test_zero_delay_allowed(self, monkeypatch, tmp_path):
|
|
cfg = self._load(monkeypatch, tmp_path, REQUEST_DELAY="0")
|
|
assert cfg.request_delay == 0.0
|
|
|
|
def test_non_integer_cap_raises(self, monkeypatch, tmp_path):
|
|
from src.config import ConfigError
|
|
with pytest.raises(ConfigError, match="MAX_CONVERSATIONS_PER_RUN"):
|
|
self._load(monkeypatch, tmp_path, MAX_CONVERSATIONS_PER_RUN="lots")
|
|
|
|
def test_zero_cap_raises(self, monkeypatch, tmp_path):
|
|
from src.config import ConfigError
|
|
with pytest.raises(ConfigError, match="at least 1"):
|
|
self._load(monkeypatch, tmp_path, MAX_CONVERSATIONS_PER_RUN="0")
|
|
|
|
def test_negative_delay_raises(self, monkeypatch, tmp_path):
|
|
from src.config import ConfigError
|
|
with pytest.raises(ConfigError, match="REQUEST_DELAY"):
|
|
self._load(monkeypatch, tmp_path, REQUEST_DELAY="-1")
|