feat: add GET/POST /api/config endpoints for IMAP account management

This commit is contained in:
pyr0ball 2026-03-04 12:03:40 -08:00
parent 1d1f25641b
commit c5a74d3821
2 changed files with 94 additions and 0 deletions

View file

@ -7,6 +7,7 @@ from __future__ import annotations
import hashlib import hashlib
import json import json
import yaml
from pathlib import Path from pathlib import Path
from datetime import datetime, timezone from datetime import datetime, timezone
@ -16,6 +17,7 @@ from pydantic import BaseModel
_ROOT = Path(__file__).parent.parent _ROOT = Path(__file__).parent.parent
_DATA_DIR: Path = _ROOT / "data" # overridable in tests via set_data_dir() _DATA_DIR: Path = _ROOT / "data" # overridable in tests via set_data_dir()
_CONFIG_DIR: Path | None = None # None = use real path
def set_data_dir(path: Path) -> None: def set_data_dir(path: Path) -> None:
@ -24,6 +26,18 @@ def set_data_dir(path: Path) -> None:
_DATA_DIR = path _DATA_DIR = path
def set_config_dir(path: Path | None) -> None:
"""Override config directory — used by tests."""
global _CONFIG_DIR
_CONFIG_DIR = path
def _config_file() -> Path:
if _CONFIG_DIR is not None:
return _CONFIG_DIR / "label_tool.yaml"
return _ROOT / "config" / "label_tool.yaml"
def reset_last_action() -> None: def reset_last_action() -> None:
"""Reset undo state — used by tests.""" """Reset undo state — used by tests."""
global _last_action global _last_action
@ -206,6 +220,31 @@ def get_labels():
return _LABEL_META return _LABEL_META
@app.get("/api/config")
def get_config():
f = _config_file()
if not f.exists():
return {"accounts": [], "max_per_account": 500}
raw = yaml.safe_load(f.read_text(encoding="utf-8")) or {}
return {"accounts": raw.get("accounts", []), "max_per_account": raw.get("max_per_account", 500)}
class ConfigPayload(BaseModel):
accounts: list[dict]
max_per_account: int = 500
@app.post("/api/config")
def post_config(payload: ConfigPayload):
f = _config_file()
f.parent.mkdir(parents=True, exist_ok=True)
tmp = f.with_suffix(".tmp")
tmp.write_text(yaml.dump(payload.model_dump(), allow_unicode=True, sort_keys=False),
encoding="utf-8")
tmp.rename(f)
return {"ok": True}
# Static SPA — MUST be last (catches all unmatched paths) # Static SPA — MUST be last (catches all unmatched paths)
_DIST = _ROOT / "web" / "dist" _DIST = _ROOT / "web" / "dist"
if _DIST.exists(): if _DIST.exists():

View file

@ -152,3 +152,58 @@ def test_config_labels_returns_metadata(client):
assert "emoji" in labels[0] assert "emoji" in labels[0]
assert "color" in labels[0] assert "color" in labels[0]
assert "name" in labels[0] assert "name" in labels[0]
# ── /api/config ──────────────────────────────────────────────────────────────
@pytest.fixture
def config_dir(tmp_path):
"""Give the API a writable config directory."""
from app import api as api_module
api_module.set_config_dir(tmp_path)
yield tmp_path
api_module.set_config_dir(None) # reset to default
@pytest.fixture
def data_dir():
"""Expose the current _DATA_DIR set by the autouse reset_globals fixture."""
from app import api as api_module
return api_module._DATA_DIR
def test_get_config_returns_empty_when_no_file(client, config_dir):
r = client.get("/api/config")
assert r.status_code == 200
data = r.json()
assert data["accounts"] == []
assert data["max_per_account"] == 500
def test_post_config_writes_yaml(client, config_dir):
import yaml
payload = {
"accounts": [{"name": "Test", "host": "imap.test.com", "port": 993,
"use_ssl": True, "username": "u@t.com", "password": "pw",
"folder": "INBOX", "days_back": 30}],
"max_per_account": 200,
}
r = client.post("/api/config", json=payload)
assert r.status_code == 200
assert r.json()["ok"] is True
cfg_file = config_dir / "label_tool.yaml"
assert cfg_file.exists()
saved = yaml.safe_load(cfg_file.read_text())
assert saved["max_per_account"] == 200
assert saved["accounts"][0]["name"] == "Test"
def test_get_config_round_trips(client, config_dir):
payload = {"accounts": [{"name": "R", "host": "h", "port": 993, "use_ssl": True,
"username": "u", "password": "p", "folder": "INBOX",
"days_back": 90}], "max_per_account": 300}
client.post("/api/config", json=payload)
r = client.get("/api/config")
data = r.json()
assert data["max_per_account"] == 300
assert data["accounts"][0]["name"] == "R"