feat: add GET/POST /api/config endpoints for IMAP account management
This commit is contained in:
parent
1d1f25641b
commit
c5a74d3821
2 changed files with 94 additions and 0 deletions
39
app/api.py
39
app/api.py
|
|
@ -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():
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue