You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
147 lines
5.7 KiB
Python
147 lines
5.7 KiB
Python
# 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
|