"""PR-Worker-Pool-Registry-1B invariant 1 강제 — voice-memo-bot wrapper 회귀 0. 테스트 5개: 1. voice-memo-bot legacy 발급 그대로 (env=true, username=voice-memo-bot → token) 2. voice-memo 분기 우선 평가 (둘 다 env true 라도 voice-memo 가 먼저 hit) 3. laptop-worker-bot env disabled → None 4. laptop-worker-bot env enabled + username match → token (TTL 365d) 5. password_changed_at 갱신 시 401 (legacy NULL 호환 포함, decode_token + verify_password_changed_at) DB 의존 0 (token 함수 + decode_token + monkeypatch only). """ from __future__ import annotations import os import sys from datetime import datetime, timedelta, timezone import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "app")) @pytest.fixture(scope="module") def anyio_backend(): return "asyncio" @pytest.fixture def env_clean(monkeypatch): for k in ( "VOICE_MEMO_BOT_TOKEN_ENABLED", "VOICE_MEMO_BOT_USERNAME", "VOICE_MEMO_BOT_TOKEN_EXPIRE_DAYS", "LAPTOP_WORKER_BOT_TOKEN_ENABLED", "LAPTOP_WORKER_BOT_USERNAME", "LAPTOP_WORKER_BOT_TOKEN_EXPIRE_DAYS", ): monkeypatch.delenv(k, raising=False) return monkeypatch def test_voice_memo_bot_legacy_unchanged(env_clean): """invariant 1 ①: voice-memo-bot env=true 시 365d token 발급 그대로.""" from core.auth import create_voice_memo_bot_token, decode_token env_clean.setenv("VOICE_MEMO_BOT_TOKEN_ENABLED", "true") token = create_voice_memo_bot_token("voice-memo-bot") assert token is not None payload = decode_token(token) assert payload is not None assert payload["sub"] == "voice-memo-bot" assert payload["type"] == "access" def test_voice_memo_branch_preferred_over_laptop(env_clean): """invariant 1 ②: 두 env 동시 true 일 때 voice-memo 분기가 먼저 평가. 실 호출 site (app/api/auth.py login) 의 분기 순서 검증: bot_token = create_voice_memo_bot_token(...) # 먼저 if bot_token is not None: return ... laptop_bot_token = create_laptop_worker_bot_token(...) # 그 다음 voice-memo-bot username 으로 호출하면 voice-memo 분기에서 return, laptop_bot_token 은 평가되지 않는다. 본 테스트는 username 별 결과로 검증. """ from core.auth import create_laptop_worker_bot_token, create_voice_memo_bot_token env_clean.setenv("VOICE_MEMO_BOT_TOKEN_ENABLED", "true") env_clean.setenv("LAPTOP_WORKER_BOT_TOKEN_ENABLED", "true") # voice-memo-bot username 에서는 voice-memo 가 hit, laptop 은 hit X assert create_voice_memo_bot_token("voice-memo-bot") is not None assert create_laptop_worker_bot_token("voice-memo-bot") is None # laptop-worker-bot username 에서는 반대 assert create_voice_memo_bot_token("laptop-worker-bot") is None assert create_laptop_worker_bot_token("laptop-worker-bot") is not None def test_laptop_worker_bot_env_disabled_returns_none(env_clean): """정정 #1 ③: env 미설정/false 일 때 None.""" from core.auth import create_laptop_worker_bot_token # env 없음 assert create_laptop_worker_bot_token("laptop-worker-bot") is None # env false env_clean.setenv("LAPTOP_WORKER_BOT_TOKEN_ENABLED", "false") assert create_laptop_worker_bot_token("laptop-worker-bot") is None # env true + username mismatch env_clean.setenv("LAPTOP_WORKER_BOT_TOKEN_ENABLED", "true") assert create_laptop_worker_bot_token("not-laptop-worker-bot") is None def test_laptop_worker_bot_env_enabled_returns_token(env_clean): """정정 #1 ④: env true + username match → 365d token.""" from core.auth import create_laptop_worker_bot_token, decode_token env_clean.setenv("LAPTOP_WORKER_BOT_TOKEN_ENABLED", "true") env_clean.setenv("LAPTOP_WORKER_BOT_TOKEN_EXPIRE_DAYS", "365") token = create_laptop_worker_bot_token("laptop-worker-bot") assert token is not None payload = decode_token(token) assert payload is not None assert payload["sub"] == "laptop-worker-bot" assert payload["type"] == "access" now_ts = int(datetime.now(timezone.utc).timestamp()) exp = int(payload["exp"]) diff_days = (exp - now_ts) / 86400 # 365d ± 1d 허용 (테스트 실행 시간 marginal) assert 364 <= diff_days <= 366, f"expected ~365d TTL, got {diff_days:.2f}d" def test_laptop_worker_bot_password_changed_at_invalidates(env_clean): """invariant 1 ⑤: password_changed_at 갱신 시 401 (legacy NULL 호환 포함).""" from fastapi import HTTPException from core.auth import ( create_laptop_worker_bot_token, decode_token, verify_password_changed_at, ) env_clean.setenv("LAPTOP_WORKER_BOT_TOKEN_ENABLED", "true") token = create_laptop_worker_bot_token("laptop-worker-bot") assert token is not None payload = decode_token(token) class FakeUser: password_changed_at: datetime | None def __init__(self, dt): self.password_changed_at = dt # ⑤-a: NULL → skip (legacy 호환) verify_password_changed_at(payload, FakeUser(None)) # raise 없음 # ⑤-b: password 변경이 token iat 보다 이전 → 통과 past = datetime.now(timezone.utc) - timedelta(hours=1) verify_password_changed_at(payload, FakeUser(past)) # raise 없음 # ⑤-c: password 변경이 token iat 보다 이후 → 401 future = datetime.now(timezone.utc) + timedelta(hours=1) with pytest.raises(HTTPException) as exc: verify_password_changed_at(payload, FakeUser(future)) assert exc.value.status_code == 401