"""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")