"""Tests for all settings API endpoints added in Tasks 1–8.""" import os import yaml import pytest from pathlib import Path from unittest.mock import patch from fastapi.testclient import TestClient # credential_store.py was merged to main repo — no worktree path manipulation needed @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", lambda: 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", lambda: 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", lambda: 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": [{"name": "linkedin", "enabled": True}]}}, f) monkeypatch.setattr("dev_api._search_prefs_path", lambda: 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", lambda: 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", lambda: 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._config_dir", lambda: fake_email_path.parent) 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._config_dir", lambda: fake_email_path.parent) 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(tmp_path, monkeypatch): """Status is 'idle' and pairs_count is 0 when no task exists.""" fake_jsonl = tmp_path / "cover_letters.jsonl" # does not exist -> 0 pairs monkeypatch.setattr("dev_api._TRAINING_JSONL", fake_jsonl) with patch("scripts.task_runner.get_task_status", return_value=None, create=True): from dev_api import app c = TestClient(app) resp = c.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", lambda: 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", lambda: 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", lambda: fake_license) 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", lambda: fake_license) 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", lambda: fake_license) 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", lambda: fake_license) 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", lambda: 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", lambda: 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