# backend/tests/test_settings.py — 설정 라우터(시스템 스냅샷 + LLM 편집/적용/테스트) + 프로필/토큰 from app import runtime_config from app.llm.provider import get_provider def test_system_snapshot_readonly(client): d = client.get("/api/settings/system").json() assert d["env"] == "dev" assert d["auth_enabled"] is False # 데모 기본 off assert d["database"] == "sqlite" assert set(d["connector_modes"]) == {"calendar", "mail"} # 기본은 전부 mock assert all(v == "mock" for v in d["connector_modes"].values()) def test_llm_config_defaults_match_env(client): d = client.get("/api/settings/llm").json() assert d["provider"] == "ollama" assert d["overridden"] is False # 오버레이 없음 → env 기본 assert "ollama" in d["provider_options"] # 보조 프로바이더 노출 assert "stt_provider" in d and "vision_provider" in d def test_llm_update_persists_and_applies(client): # ollama + 커스텀 모델로 저장 → 영속 + 런타임 반영 r = client.put("/api/settings/llm", json={"provider": "ollama", "model": "my-model"}) assert r.status_code == 200 body = r.json() assert body["provider"] == "ollama" assert body["model"] == "my-model" assert body["overridden"] is True # 다시 GET 해도 유지(영속) again = client.get("/api/settings/llm").json() assert again["provider"] == "ollama" and again["model"] == "my-model" # 런타임 오버레이가 실제 provider 선택에 반영되는가(force 없이) assert runtime_config.effective_llm().provider == "ollama" assert get_provider().name == "ollama" def test_llm_update_reset_to_default(client): client.put("/api/settings/llm", json={"provider": "ollama"}) assert client.get("/api/settings/llm").json()["provider"] == "ollama" # 빈 문자열 → 기본으로 되돌리기 r = client.put("/api/settings/llm", json={"provider": ""}) assert r.status_code == 200 assert r.json()["overridden"] is False assert runtime_config.snapshot() == {} def test_llm_update_rejects_bad_provider(client): r = client.put("/api/settings/llm", json={"provider": "gpt5"}) assert r.status_code == 400 def test_llm_test_uses_ollama(client): # phase-16+: 휴리스틱 옵션 제거 → 연결 테스트는 항상 Ollama 도달성으로 확인. r = client.post("/api/settings/llm/test", json={"provider": "ollama"}) assert r.status_code == 200 d = r.json() assert d["provider"] == "ollama" assert isinstance(d["reachable"], bool) # 실 Ollama 가동 여부에 따라 달라짐(환경 의존) def test_llm_provider_options_include_openai_gemini(client): d = client.get("/api/settings/llm").json() assert "openai" in d["provider_options"] assert "gemini" in d["provider_options"] assert d["api_key_set"] is False # 기본은 키 없음 def test_llm_openai_with_api_key_encrypted(client, session): s, _ = session r = client.put( "/api/settings/llm", json={ "provider": "openai", "host": "https://api.openai.com/v1", "model": "gpt-4o-mini", "api_key": "sk-secret-xyz", }, ) assert r.status_code == 200 body = r.json() assert body["provider"] == "openai" and body["model"] == "gpt-4o-mini" assert body["api_key_set"] is True assert "api_key" not in body # 키 값은 절대 응답에 없음 # 유효 설정/팩토리에 반영(복호화 라운드트립) eff = runtime_config.effective_llm() assert eff.provider == "openai" and eff.api_key == "sk-secret-xyz" assert get_provider().name == "openai" # 저장은 암호화(평문 금지) from app.models import AppSetting row = s.get(AppSetting, "llm.api_key") assert row is not None and row.value.startswith("enc:") and "sk-secret-xyz" not in row.value def test_llm_gemini_provider_select(client): r = client.put("/api/settings/llm", json={"provider": "gemini", "model": "gemini-2.5-flash"}) assert r.status_code == 200 and r.json()["provider"] == "gemini" assert runtime_config.effective_llm().provider == "gemini" assert get_provider().name == "gemini" def test_llm_api_key_cleared_on_reset(client): client.put("/api/settings/llm", json={"provider": "openai", "api_key": "sk-zzz"}) assert client.get("/api/settings/llm").json()["api_key_set"] is True # 빈 문자열들 → 전부 기본으로 되돌리기(키 삭제 포함) client.put( "/api/settings/llm", json={"provider": "", "model": "", "host": "", "timeout": 0, "api_key": ""}, ) d = client.get("/api/settings/llm").json() assert d["api_key_set"] is False and d["provider"] == "ollama" def test_profile_update(client): r = client.patch("/api/me", json={"name": "지우님", "role": "프로덕트 리드"}) assert r.status_code == 200 d = r.json() assert d["name"] == "지우님" assert d["initial"] == "지" assert d["role"] == "프로덕트 리드" # 영속 확인 assert client.get("/api/me").json()["role"] == "프로덕트 리드" def test_api_tokens_create_list_revoke(client): # 처음엔 토큰 없음 assert client.get("/api/me/tokens").json() == [] # 생성 → 평문 1회 노출 created = client.post("/api/me/tokens", params={"name": "CLI"}).json() assert created["token"] and created["name"] == "CLI" tid = created["id"] # 목록(평문 미포함) rows = client.get("/api/me/tokens").json() assert len(rows) == 1 and rows[0]["id"] == tid and "token" not in rows[0] assert rows[0]["revoked"] is False # 폐기 assert client.delete(f"/api/me/tokens/{tid}").status_code == 200 assert client.get("/api/me/tokens").json()[0]["revoked"] is True