"""FlowDeck — v5.3.0 tests: inline databases, database templates, property validation.""" import json import os import tempfile import pytest from conftest import login_test_client from fastapi.testclient import TestClient @pytest.fixture def client(): db_file = tempfile.NamedTemporaryFile(suffix=".db", delete=False) db_path = db_file.name db_file.close() os.environ["DATABASE_URL"] = f"sqlite:///{db_path}" os.environ["APP_SECRET_KEY"] = "test-secret-for-tests" os.environ["RATE_LIMIT_ENABLED"] = "false" os.environ["PUBLIC_API_INSECURE_OK"] = "true" # Point the process-wide settings singleton at OUR temp DB (xdist-safe). from app.config import settings settings.database_url = f"sqlite:///{db_path}" settings.rate_limit_enabled = False settings.public_api_insecure_ok = True from app.db import init_db from app.main import app init_db() yield login_test_client(TestClient(app)) os.unlink(db_path) # ── Database templates ── def test_seed_templates_available(client): resp = client.get("/workspace/templates/database") assert resp.status_code == 200 templates = resp.json()["templates"] names = {t["name"] for t in templates} assert "Project tracker" in names assert "CRM / Contacts" in names for t in templates: assert t.get("icon") def _props(client, cid): return client.get(f"/db/{cid}/properties/api").json()["properties"] def test_create_collection_from_template_materializes_properties(client): resp = client.post("/db/api", json={"name": "My CRM", "template": "CRM / Contacts"}) assert resp.status_code == 200 cid = resp.json()["id"] # Properties materialized (status + person/email etc.), title skipped props = _props(client, cid) names = {p["name"]: p for p in props} assert "Email" in names assert "Stage" in names assert names["Stage"]["prop_type"] == "status" assert "Title" not in names # title is the row title, not a column def test_create_inline_database_from_template(client): resp = client.post("/db/inline/api", json={"name": "Tasks", "template": "Task list", "parent_page_id": 1}) assert resp.status_code == 200 cid = resp.json()["id"] assert resp.json()["is_inline"] is True props = _props(client, cid) names = {p["name"] for p in props} assert {"Status", "Priority", "Due date"} <= names def test_apply_db_template_endpoint(client): resp = client.get("/workspace/templates/database").json() tpl = next(t for t in resp["templates"] if t["name"] == "Project tracker") r = client.post(f"/workspace/templates/database/{tpl['id']}/apply", json={"name": "Proj"}) assert r.status_code == 200 cid = r.json()["collection_id"] props = _props(client, cid) names = {p["name"] for p in props} assert {"Status", "Priority", "Assignee"} <= names # ── Property validation ── def _add_collection(client, name="V"): return client.post("/db/api", json={"name": name}).json()["id"] def test_required_property_validation(client): cid = _add_collection(client) client.post(f"/db/{cid}/properties/api", json={"name": "Email", "prop_type": "email", "validation": {"required": True}}) # Missing required → 400 r = client.post(f"/db/{cid}/pages/api", json={"title": "Row", "properties": {}}) assert r.status_code == 400 assert "required" in r.json()["detail"].lower() # With value → ok r = client.post(f"/db/{cid}/pages/api", json={"title": "Row", "properties": {"Email": "a@b.com"}}) assert r.status_code == 200 def test_unique_property_validation(client): cid = _add_collection(client) client.post(f"/db/{cid}/properties/api", json={"name": "Code", "prop_type": "text", "validation": {"unique": True}}) client.post(f"/db/{cid}/pages/api", json={"title": "R1", "properties": {"Code": "abc"}}) # Duplicate → 400 r = client.post(f"/db/{cid}/pages/api", json={"title": "R2", "properties": {"Code": "abc"}}) assert r.status_code == 400 assert "unique" in r.json()["detail"].lower() # Update to a duplicate also fails pid = client.post(f"/db/{cid}/pages/api", json={"title": "R3", "properties": {"Code": "xyz"}}).json()["id"] r = client.put(f"/db/pages/{pid}/api", json={"properties": {"Code": "abc"}}) assert r.status_code == 400 # Same row keeping its own value is allowed r = client.put(f"/db/pages/{pid}/api", json={"properties": {"Code": "xyz"}}) assert r.status_code == 200 def test_min_max_validation(client): cid = _add_collection(client) client.post(f"/db/{cid}/properties/api", json={"name": "Score", "prop_type": "number", "validation": {"min": 0, "max": 100}}) r = client.post(f"/db/{cid}/pages/api", json={"title": "R", "properties": {"Score": -5}}) assert r.status_code == 400 r = client.post(f"/db/{cid}/pages/api", json={"title": "R", "properties": {"Score": 150}}) assert r.status_code == 400 r = client.post(f"/db/{cid}/pages/api", json={"title": "R", "properties": {"Score": 50}}) assert r.status_code == 200 def test_property_validation_persisted_and_readable(client): cid = _add_collection(client) r = client.post(f"/db/{cid}/properties/api", json={"name": "P", "prop_type": "number", "validation": {"min": 1, "max": 9}}) pid = r.json()["id"] props = _props(client, cid) p = next(x for x in props if x["id"] == pid) assert json.loads(p["validation_json"]) == {"min": 1, "max": 9} # ── Inline database block data (via /db collection payload) ── def test_inline_db_collection_payload(client): cid = _add_collection(client) r = client.get(f"/db/{cid}/api") assert r.status_code == 200 data = r.json() assert data["collection"]["id"] == cid assert "views" in data