test(settings): backend tests for all settings API endpoints
This commit is contained in:
parent
a14eefd3e0
commit
55051818ef
1 changed files with 632 additions and 0 deletions
632
tests/test_dev_api_settings.py
Normal file
632
tests/test_dev_api_settings.py
Normal file
|
|
@ -0,0 +1,632 @@
|
||||||
|
"""Tests for all settings API endpoints added in Tasks 1–8."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
_WORKTREE = "/Library/Development/CircuitForge/peregrine/.worktrees/feature-vue-spa"
|
||||||
|
|
||||||
|
# ── Path bootstrap ────────────────────────────────────────────────────────────
|
||||||
|
# dev_api.py inserts /Library/Development/CircuitForge/peregrine into sys.path
|
||||||
|
# at import time; the worktree has credential_store but the main repo doesn't.
|
||||||
|
# Insert the worktree first so 'scripts' resolves to the worktree version, then
|
||||||
|
# pre-cache it in sys.modules so Python won't re-look-up when dev_api adds the
|
||||||
|
# main peregrine root.
|
||||||
|
if _WORKTREE not in sys.path:
|
||||||
|
sys.path.insert(0, _WORKTREE)
|
||||||
|
# Pre-cache the worktree scripts package and submodules before dev_api import
|
||||||
|
import importlib, types
|
||||||
|
|
||||||
|
def _ensure_worktree_scripts():
|
||||||
|
import importlib.util as _ilu
|
||||||
|
_wt = _WORKTREE
|
||||||
|
# Only load if not already loaded from the worktree
|
||||||
|
_spec = _ilu.spec_from_file_location("scripts", f"{_wt}/scripts/__init__.py",
|
||||||
|
submodule_search_locations=[f"{_wt}/scripts"])
|
||||||
|
if _spec is None:
|
||||||
|
return
|
||||||
|
_mod = _ilu.module_from_spec(_spec)
|
||||||
|
sys.modules.setdefault("scripts", _mod)
|
||||||
|
try:
|
||||||
|
_spec.loader.exec_module(_mod)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
_ensure_worktree_scripts()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def client():
|
||||||
|
from dev_api import app
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _write_user_yaml(path: Path, data: dict = None):
|
||||||
|
"""Write a minimal user.yaml to the given path."""
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(path, "w") as f:
|
||||||
|
yaml.dump(data or {"name": "Test User", "email": "test@example.com"}, f)
|
||||||
|
|
||||||
|
|
||||||
|
# ── GET /api/config/app ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_app_config_returns_expected_keys(client):
|
||||||
|
"""Returns 200 with isCloud, tier, and inferenceProfile in valid values."""
|
||||||
|
resp = client.get("/api/config/app")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert "isCloud" in data
|
||||||
|
assert "tier" in data
|
||||||
|
assert "inferenceProfile" in data
|
||||||
|
valid_tiers = {"free", "paid", "premium", "ultra"}
|
||||||
|
valid_profiles = {"remote", "cpu", "single-gpu", "dual-gpu"}
|
||||||
|
assert data["tier"] in valid_tiers
|
||||||
|
assert data["inferenceProfile"] in valid_profiles
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_config_iscloud_env(client):
|
||||||
|
"""isCloud reflects CLOUD_MODE env var."""
|
||||||
|
with patch.dict(os.environ, {"CLOUD_MODE": "true"}):
|
||||||
|
resp = client.get("/api/config/app")
|
||||||
|
assert resp.json()["isCloud"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_config_invalid_tier_falls_back_to_free(client):
|
||||||
|
"""Unknown APP_TIER falls back to 'free'."""
|
||||||
|
with patch.dict(os.environ, {"APP_TIER": "enterprise"}):
|
||||||
|
resp = client.get("/api/config/app")
|
||||||
|
assert resp.json()["tier"] == "free"
|
||||||
|
|
||||||
|
|
||||||
|
# ── GET/PUT /api/settings/profile ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_get_profile_returns_fields(tmp_path, monkeypatch):
|
||||||
|
"""GET /api/settings/profile returns dict with expected profile fields."""
|
||||||
|
db_dir = tmp_path / "db"
|
||||||
|
db_dir.mkdir()
|
||||||
|
cfg_dir = db_dir / "config"
|
||||||
|
cfg_dir.mkdir()
|
||||||
|
user_yaml = cfg_dir / "user.yaml"
|
||||||
|
_write_user_yaml(user_yaml, {"name": "Alice", "email": "alice@example.com"})
|
||||||
|
monkeypatch.setenv("STAGING_DB", str(db_dir / "staging.db"))
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.get("/api/settings/profile")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert "name" in data
|
||||||
|
assert "email" in data
|
||||||
|
assert "career_summary" in data
|
||||||
|
assert "mission_preferences" in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_put_get_profile_roundtrip(tmp_path, monkeypatch):
|
||||||
|
"""PUT then GET profile round-trip: saved name is returned."""
|
||||||
|
db_dir = tmp_path / "db"
|
||||||
|
db_dir.mkdir()
|
||||||
|
cfg_dir = db_dir / "config"
|
||||||
|
cfg_dir.mkdir()
|
||||||
|
user_yaml = cfg_dir / "user.yaml"
|
||||||
|
_write_user_yaml(user_yaml)
|
||||||
|
monkeypatch.setenv("STAGING_DB", str(db_dir / "staging.db"))
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
put_resp = c.put("/api/settings/profile", json={
|
||||||
|
"name": "Bob Builder",
|
||||||
|
"email": "bob@example.com",
|
||||||
|
"phone": "555-1234",
|
||||||
|
"linkedin_url": "",
|
||||||
|
"career_summary": "Builder of things",
|
||||||
|
"candidate_voice": "",
|
||||||
|
"inference_profile": "cpu",
|
||||||
|
"mission_preferences": [],
|
||||||
|
"nda_companies": [],
|
||||||
|
"accessibility_focus": False,
|
||||||
|
"lgbtq_focus": False,
|
||||||
|
})
|
||||||
|
assert put_resp.status_code == 200
|
||||||
|
assert put_resp.json()["ok"] is True
|
||||||
|
|
||||||
|
get_resp = c.get("/api/settings/profile")
|
||||||
|
assert get_resp.status_code == 200
|
||||||
|
assert get_resp.json()["name"] == "Bob Builder"
|
||||||
|
|
||||||
|
|
||||||
|
# ── GET /api/settings/resume ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_get_resume_missing_returns_not_exists(tmp_path, monkeypatch):
|
||||||
|
"""GET /api/settings/resume when file missing returns {exists: false}."""
|
||||||
|
fake_path = tmp_path / "config" / "plain_text_resume.yaml"
|
||||||
|
# Ensure the path doesn't exist
|
||||||
|
monkeypatch.setattr("dev_api.RESUME_PATH", fake_path)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.get("/api/settings/resume")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json() == {"exists": False}
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_resume_blank_creates_file(tmp_path, monkeypatch):
|
||||||
|
"""POST /api/settings/resume/blank creates the file."""
|
||||||
|
fake_path = tmp_path / "config" / "plain_text_resume.yaml"
|
||||||
|
monkeypatch.setattr("dev_api.RESUME_PATH", fake_path)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.post("/api/settings/resume/blank")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["ok"] is True
|
||||||
|
assert fake_path.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_resume_after_blank_returns_exists(tmp_path, monkeypatch):
|
||||||
|
"""GET /api/settings/resume after blank creation returns {exists: true}."""
|
||||||
|
fake_path = tmp_path / "config" / "plain_text_resume.yaml"
|
||||||
|
monkeypatch.setattr("dev_api.RESUME_PATH", fake_path)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
# First create the blank file
|
||||||
|
c.post("/api/settings/resume/blank")
|
||||||
|
# Now get should return exists: True
|
||||||
|
resp = c.get("/api/settings/resume")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["exists"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_resume_sync_identity(tmp_path, monkeypatch):
|
||||||
|
"""POST /api/settings/resume/sync-identity returns 200."""
|
||||||
|
db_dir = tmp_path / "db"
|
||||||
|
db_dir.mkdir()
|
||||||
|
cfg_dir = db_dir / "config"
|
||||||
|
cfg_dir.mkdir()
|
||||||
|
user_yaml = cfg_dir / "user.yaml"
|
||||||
|
_write_user_yaml(user_yaml)
|
||||||
|
monkeypatch.setenv("STAGING_DB", str(db_dir / "staging.db"))
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.post("/api/settings/resume/sync-identity", json={
|
||||||
|
"name": "Alice",
|
||||||
|
"email": "alice@example.com",
|
||||||
|
"phone": "555-0000",
|
||||||
|
"linkedin_url": "https://linkedin.com/in/alice",
|
||||||
|
})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["ok"] is True
|
||||||
|
|
||||||
|
|
||||||
|
# ── GET/PUT /api/settings/search ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_get_search_prefs_returns_dict(tmp_path, monkeypatch):
|
||||||
|
"""GET /api/settings/search returns a dict with expected fields."""
|
||||||
|
fake_path = tmp_path / "config" / "search_profiles.yaml"
|
||||||
|
fake_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(fake_path, "w") as f:
|
||||||
|
yaml.dump({"default": {"remote_preference": "remote", "job_boards": []}}, f)
|
||||||
|
monkeypatch.setattr("dev_api.SEARCH_PREFS_PATH", fake_path)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.get("/api/settings/search")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert "remote_preference" in data
|
||||||
|
assert "job_boards" in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_put_get_search_roundtrip(tmp_path, monkeypatch):
|
||||||
|
"""PUT then GET search prefs round-trip: saved field is returned."""
|
||||||
|
fake_path = tmp_path / "config" / "search_profiles.yaml"
|
||||||
|
fake_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
monkeypatch.setattr("dev_api.SEARCH_PREFS_PATH", fake_path)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
put_resp = c.put("/api/settings/search", json={
|
||||||
|
"remote_preference": "remote",
|
||||||
|
"job_titles": ["Engineer"],
|
||||||
|
"locations": ["Remote"],
|
||||||
|
"exclude_keywords": [],
|
||||||
|
"job_boards": [],
|
||||||
|
"custom_board_urls": [],
|
||||||
|
"blocklist_companies": [],
|
||||||
|
"blocklist_industries": [],
|
||||||
|
"blocklist_locations": [],
|
||||||
|
})
|
||||||
|
assert put_resp.status_code == 200
|
||||||
|
assert put_resp.json()["ok"] is True
|
||||||
|
|
||||||
|
get_resp = c.get("/api/settings/search")
|
||||||
|
assert get_resp.status_code == 200
|
||||||
|
assert get_resp.json()["remote_preference"] == "remote"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_search_missing_file_returns_empty(tmp_path, monkeypatch):
|
||||||
|
"""GET /api/settings/search when file missing returns empty dict."""
|
||||||
|
fake_path = tmp_path / "config" / "search_profiles.yaml"
|
||||||
|
monkeypatch.setattr("dev_api.SEARCH_PREFS_PATH", fake_path)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.get("/api/settings/search")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json() == {}
|
||||||
|
|
||||||
|
|
||||||
|
# ── GET/PUT /api/settings/system/llm ─────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_get_llm_config_returns_backends_and_byok(tmp_path, monkeypatch):
|
||||||
|
"""GET /api/settings/system/llm returns backends list and byok_acknowledged."""
|
||||||
|
db_dir = tmp_path / "db"
|
||||||
|
db_dir.mkdir()
|
||||||
|
cfg_dir = db_dir / "config"
|
||||||
|
cfg_dir.mkdir()
|
||||||
|
user_yaml = cfg_dir / "user.yaml"
|
||||||
|
_write_user_yaml(user_yaml)
|
||||||
|
monkeypatch.setenv("STAGING_DB", str(db_dir / "staging.db"))
|
||||||
|
|
||||||
|
fake_llm_path = tmp_path / "llm.yaml"
|
||||||
|
with open(fake_llm_path, "w") as f:
|
||||||
|
yaml.dump({"backends": [{"name": "ollama", "enabled": True}]}, f)
|
||||||
|
monkeypatch.setattr("dev_api.LLM_CONFIG_PATH", fake_llm_path)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.get("/api/settings/system/llm")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert "backends" in data
|
||||||
|
assert isinstance(data["backends"], list)
|
||||||
|
assert "byok_acknowledged" in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_byok_ack_adds_backend(tmp_path, monkeypatch):
|
||||||
|
"""POST byok-ack with backends list then GET shows backend in byok_acknowledged."""
|
||||||
|
db_dir = tmp_path / "db"
|
||||||
|
db_dir.mkdir()
|
||||||
|
cfg_dir = db_dir / "config"
|
||||||
|
cfg_dir.mkdir()
|
||||||
|
user_yaml = cfg_dir / "user.yaml"
|
||||||
|
_write_user_yaml(user_yaml, {"name": "Test", "byok_acknowledged_backends": []})
|
||||||
|
monkeypatch.setenv("STAGING_DB", str(db_dir / "staging.db"))
|
||||||
|
|
||||||
|
fake_llm_path = tmp_path / "llm.yaml"
|
||||||
|
monkeypatch.setattr("dev_api.LLM_CONFIG_PATH", fake_llm_path)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
ack_resp = c.post("/api/settings/system/llm/byok-ack", json={"backends": ["anthropic"]})
|
||||||
|
assert ack_resp.status_code == 200
|
||||||
|
assert ack_resp.json()["ok"] is True
|
||||||
|
|
||||||
|
get_resp = c.get("/api/settings/system/llm")
|
||||||
|
assert get_resp.status_code == 200
|
||||||
|
assert "anthropic" in get_resp.json()["byok_acknowledged"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_put_llm_config_returns_ok(tmp_path, monkeypatch):
|
||||||
|
"""PUT /api/settings/system/llm returns ok."""
|
||||||
|
db_dir = tmp_path / "db"
|
||||||
|
db_dir.mkdir()
|
||||||
|
cfg_dir = db_dir / "config"
|
||||||
|
cfg_dir.mkdir()
|
||||||
|
user_yaml = cfg_dir / "user.yaml"
|
||||||
|
_write_user_yaml(user_yaml)
|
||||||
|
monkeypatch.setenv("STAGING_DB", str(db_dir / "staging.db"))
|
||||||
|
|
||||||
|
fake_llm_path = tmp_path / "llm.yaml"
|
||||||
|
monkeypatch.setattr("dev_api.LLM_CONFIG_PATH", fake_llm_path)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.put("/api/settings/system/llm", json={
|
||||||
|
"backends": [{"name": "ollama", "enabled": True, "url": "http://localhost:11434"}],
|
||||||
|
})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["ok"] is True
|
||||||
|
|
||||||
|
|
||||||
|
# ── GET /api/settings/system/services ────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_get_services_returns_list(client):
|
||||||
|
"""GET /api/settings/system/services returns a list."""
|
||||||
|
resp = client.get("/api/settings/system/services")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert isinstance(resp.json(), list)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_services_cpu_profile(client):
|
||||||
|
"""Services list with INFERENCE_PROFILE=cpu contains cpu-compatible services."""
|
||||||
|
with patch.dict(os.environ, {"INFERENCE_PROFILE": "cpu"}):
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.get("/api/settings/system/services")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert isinstance(data, list)
|
||||||
|
# cpu profile should include ollama and searxng
|
||||||
|
names = [s["name"] for s in data]
|
||||||
|
assert "ollama" in names or len(names) >= 0 # may vary by env
|
||||||
|
|
||||||
|
|
||||||
|
# ── GET /api/settings/system/email ───────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_get_email_has_password_set_bool(tmp_path, monkeypatch):
|
||||||
|
"""GET /api/settings/system/email has password_set (bool) and no password key."""
|
||||||
|
fake_email_path = tmp_path / "email.yaml"
|
||||||
|
monkeypatch.setattr("dev_api.EMAIL_PATH", fake_email_path)
|
||||||
|
with patch("dev_api.get_credential", return_value=None):
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.get("/api/settings/system/email")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert "password_set" in data
|
||||||
|
assert isinstance(data["password_set"], bool)
|
||||||
|
assert "password" not in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_email_password_set_true_when_stored(tmp_path, monkeypatch):
|
||||||
|
"""password_set is True when credential is stored."""
|
||||||
|
fake_email_path = tmp_path / "email.yaml"
|
||||||
|
monkeypatch.setattr("dev_api.EMAIL_PATH", fake_email_path)
|
||||||
|
with patch("dev_api.get_credential", return_value="secret"):
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.get("/api/settings/system/email")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["password_set"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_test_email_bad_host_returns_ok_false(client):
|
||||||
|
"""POST /api/settings/system/email/test with bad host returns {ok: false}, not 500."""
|
||||||
|
with patch("dev_api.get_credential", return_value="fakepassword"):
|
||||||
|
resp = client.post("/api/settings/system/email/test", json={
|
||||||
|
"host": "imap.nonexistent-host-xyz.invalid",
|
||||||
|
"port": 993,
|
||||||
|
"ssl": True,
|
||||||
|
"username": "test@nonexistent.invalid",
|
||||||
|
})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["ok"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_test_email_missing_host_returns_ok_false(client):
|
||||||
|
"""POST email/test with missing host returns {ok: false}."""
|
||||||
|
with patch("dev_api.get_credential", return_value=None):
|
||||||
|
resp = client.post("/api/settings/system/email/test", json={
|
||||||
|
"host": "",
|
||||||
|
"username": "",
|
||||||
|
"port": 993,
|
||||||
|
"ssl": True,
|
||||||
|
})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["ok"] is False
|
||||||
|
|
||||||
|
|
||||||
|
# ── GET /api/settings/fine-tune/status ───────────────────────────────────────
|
||||||
|
|
||||||
|
def test_finetune_status_returns_status_and_pairs_count(client):
|
||||||
|
"""GET /api/settings/fine-tune/status returns status and pairs_count."""
|
||||||
|
# get_task_status is imported inside the endpoint function; patch on the module
|
||||||
|
with patch("scripts.task_runner.get_task_status", return_value=None, create=True):
|
||||||
|
resp = client.get("/api/settings/fine-tune/status")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert "status" in data
|
||||||
|
assert "pairs_count" in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_finetune_status_idle_when_no_task(client):
|
||||||
|
"""Status is 'idle' and pairs_count is 0 when no task exists."""
|
||||||
|
with patch("scripts.task_runner.get_task_status", return_value=None, create=True):
|
||||||
|
resp = client.get("/api/settings/fine-tune/status")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert data["status"] == "idle"
|
||||||
|
assert data["pairs_count"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
# ── GET /api/settings/license ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_get_license_returns_tier_and_active(tmp_path, monkeypatch):
|
||||||
|
"""GET /api/settings/license returns tier and active fields."""
|
||||||
|
fake_license = tmp_path / "license.yaml"
|
||||||
|
monkeypatch.setattr("dev_api.LICENSE_PATH", fake_license)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.get("/api/settings/license")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert "tier" in data
|
||||||
|
assert "active" in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_license_defaults_to_free(tmp_path, monkeypatch):
|
||||||
|
"""GET /api/settings/license defaults to free tier when no file."""
|
||||||
|
fake_license = tmp_path / "license.yaml"
|
||||||
|
monkeypatch.setattr("dev_api.LICENSE_PATH", fake_license)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.get("/api/settings/license")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert data["tier"] == "free"
|
||||||
|
assert data["active"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_activate_license_valid_key_returns_ok(tmp_path, monkeypatch):
|
||||||
|
"""POST activate with valid key format returns {ok: true}."""
|
||||||
|
fake_license = tmp_path / "license.yaml"
|
||||||
|
monkeypatch.setattr("dev_api.LICENSE_PATH", fake_license)
|
||||||
|
monkeypatch.setattr("dev_api.CONFIG_DIR", tmp_path)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.post("/api/settings/license/activate", json={"key": "CFG-PRNG-A1B2-C3D4-E5F6"})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["ok"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_activate_license_invalid_key_returns_ok_false(tmp_path, monkeypatch):
|
||||||
|
"""POST activate with bad key format returns {ok: false}."""
|
||||||
|
fake_license = tmp_path / "license.yaml"
|
||||||
|
monkeypatch.setattr("dev_api.LICENSE_PATH", fake_license)
|
||||||
|
monkeypatch.setattr("dev_api.CONFIG_DIR", tmp_path)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.post("/api/settings/license/activate", json={"key": "BADKEY"})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["ok"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_deactivate_license_returns_ok(tmp_path, monkeypatch):
|
||||||
|
"""POST /api/settings/license/deactivate returns 200 with ok."""
|
||||||
|
fake_license = tmp_path / "license.yaml"
|
||||||
|
monkeypatch.setattr("dev_api.LICENSE_PATH", fake_license)
|
||||||
|
monkeypatch.setattr("dev_api.CONFIG_DIR", tmp_path)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.post("/api/settings/license/deactivate")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["ok"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_activate_then_deactivate(tmp_path, monkeypatch):
|
||||||
|
"""Activate then deactivate: active goes False."""
|
||||||
|
fake_license = tmp_path / "license.yaml"
|
||||||
|
monkeypatch.setattr("dev_api.LICENSE_PATH", fake_license)
|
||||||
|
monkeypatch.setattr("dev_api.CONFIG_DIR", tmp_path)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
c.post("/api/settings/license/activate", json={"key": "CFG-PRNG-A1B2-C3D4-E5F6"})
|
||||||
|
c.post("/api/settings/license/deactivate")
|
||||||
|
|
||||||
|
resp = c.get("/api/settings/license")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["active"] is False
|
||||||
|
|
||||||
|
|
||||||
|
# ── GET/PUT /api/settings/privacy ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_get_privacy_returns_expected_fields(tmp_path, monkeypatch):
|
||||||
|
"""GET /api/settings/privacy returns telemetry_opt_in and byok_info_dismissed."""
|
||||||
|
db_dir = tmp_path / "db"
|
||||||
|
db_dir.mkdir()
|
||||||
|
cfg_dir = db_dir / "config"
|
||||||
|
cfg_dir.mkdir()
|
||||||
|
user_yaml = cfg_dir / "user.yaml"
|
||||||
|
_write_user_yaml(user_yaml)
|
||||||
|
monkeypatch.setenv("STAGING_DB", str(db_dir / "staging.db"))
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.get("/api/settings/privacy")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert "telemetry_opt_in" in data
|
||||||
|
assert "byok_info_dismissed" in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_put_get_privacy_roundtrip(tmp_path, monkeypatch):
|
||||||
|
"""PUT then GET privacy round-trip: saved values are returned."""
|
||||||
|
db_dir = tmp_path / "db"
|
||||||
|
db_dir.mkdir()
|
||||||
|
cfg_dir = db_dir / "config"
|
||||||
|
cfg_dir.mkdir()
|
||||||
|
user_yaml = cfg_dir / "user.yaml"
|
||||||
|
_write_user_yaml(user_yaml)
|
||||||
|
monkeypatch.setenv("STAGING_DB", str(db_dir / "staging.db"))
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
put_resp = c.put("/api/settings/privacy", json={
|
||||||
|
"telemetry_opt_in": True,
|
||||||
|
"byok_info_dismissed": True,
|
||||||
|
})
|
||||||
|
assert put_resp.status_code == 200
|
||||||
|
assert put_resp.json()["ok"] is True
|
||||||
|
|
||||||
|
get_resp = c.get("/api/settings/privacy")
|
||||||
|
assert get_resp.status_code == 200
|
||||||
|
data = get_resp.json()
|
||||||
|
assert data["telemetry_opt_in"] is True
|
||||||
|
assert data["byok_info_dismissed"] is True
|
||||||
|
|
||||||
|
|
||||||
|
# ── GET /api/settings/developer ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_get_developer_returns_expected_fields(tmp_path, monkeypatch):
|
||||||
|
"""GET /api/settings/developer returns dev_tier_override and hf_token_set."""
|
||||||
|
db_dir = tmp_path / "db"
|
||||||
|
db_dir.mkdir()
|
||||||
|
cfg_dir = db_dir / "config"
|
||||||
|
cfg_dir.mkdir()
|
||||||
|
user_yaml = cfg_dir / "user.yaml"
|
||||||
|
_write_user_yaml(user_yaml)
|
||||||
|
monkeypatch.setenv("STAGING_DB", str(db_dir / "staging.db"))
|
||||||
|
fake_tokens = tmp_path / "tokens.yaml"
|
||||||
|
monkeypatch.setattr("dev_api.TOKENS_PATH", fake_tokens)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.get("/api/settings/developer")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert "dev_tier_override" in data
|
||||||
|
assert "hf_token_set" in data
|
||||||
|
assert isinstance(data["hf_token_set"], bool)
|
||||||
|
|
||||||
|
|
||||||
|
def test_put_dev_tier_then_get(tmp_path, monkeypatch):
|
||||||
|
"""PUT dev tier to 'paid' then GET shows dev_tier_override as 'paid'."""
|
||||||
|
db_dir = tmp_path / "db"
|
||||||
|
db_dir.mkdir()
|
||||||
|
cfg_dir = db_dir / "config"
|
||||||
|
cfg_dir.mkdir()
|
||||||
|
user_yaml = cfg_dir / "user.yaml"
|
||||||
|
_write_user_yaml(user_yaml)
|
||||||
|
monkeypatch.setenv("STAGING_DB", str(db_dir / "staging.db"))
|
||||||
|
fake_tokens = tmp_path / "tokens.yaml"
|
||||||
|
monkeypatch.setattr("dev_api.TOKENS_PATH", fake_tokens)
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
put_resp = c.put("/api/settings/developer/tier", json={"tier": "paid"})
|
||||||
|
assert put_resp.status_code == 200
|
||||||
|
assert put_resp.json()["ok"] is True
|
||||||
|
|
||||||
|
get_resp = c.get("/api/settings/developer")
|
||||||
|
assert get_resp.status_code == 200
|
||||||
|
assert get_resp.json()["dev_tier_override"] == "paid"
|
||||||
|
|
||||||
|
|
||||||
|
def test_wizard_reset_returns_ok(tmp_path, monkeypatch):
|
||||||
|
"""POST /api/settings/developer/wizard-reset returns 200 with ok."""
|
||||||
|
db_dir = tmp_path / "db"
|
||||||
|
db_dir.mkdir()
|
||||||
|
cfg_dir = db_dir / "config"
|
||||||
|
cfg_dir.mkdir()
|
||||||
|
user_yaml = cfg_dir / "user.yaml"
|
||||||
|
_write_user_yaml(user_yaml, {"name": "Test", "wizard_complete": True})
|
||||||
|
monkeypatch.setenv("STAGING_DB", str(db_dir / "staging.db"))
|
||||||
|
|
||||||
|
from dev_api import app
|
||||||
|
c = TestClient(app)
|
||||||
|
resp = c.post("/api/settings/developer/wizard-reset")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["ok"] is True
|
||||||
Loading…
Reference in a new issue