test: messaging HTTP integration tests (#74)
This commit is contained in:
parent
715a8aa33e
commit
e11750e0e6
1 changed files with 196 additions and 0 deletions
196
tests/test_messaging_integration.py
Normal file
196
tests/test_messaging_integration.py
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
"""Integration tests for messaging endpoints."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from scripts.db_migrate import migrate_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fresh_db(tmp_path, monkeypatch):
|
||||
"""Set up a fresh isolated DB wired to dev_api._request_db."""
|
||||
db = tmp_path / "test.db"
|
||||
monkeypatch.setenv("STAGING_DB", str(db))
|
||||
migrate_db(db)
|
||||
import dev_api
|
||||
monkeypatch.setattr(
|
||||
dev_api,
|
||||
"_request_db",
|
||||
type("CV", (), {"get": lambda self: str(db), "set": lambda *a: None})(),
|
||||
)
|
||||
monkeypatch.setattr(dev_api, "DB_PATH", str(db))
|
||||
return db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(fresh_db):
|
||||
import dev_api
|
||||
return TestClient(dev_api.app)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Messages
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_create_and_list_message(client):
|
||||
"""POST /api/messages creates a row; GET /api/messages?job_id= returns it."""
|
||||
payload = {
|
||||
"job_id": 1,
|
||||
"type": "email",
|
||||
"direction": "outbound",
|
||||
"subject": "Hello recruiter",
|
||||
"body": "I am very interested in this role.",
|
||||
"to_addr": "recruiter@example.com",
|
||||
}
|
||||
resp = client.post("/api/messages", json=payload)
|
||||
assert resp.status_code == 200, resp.text
|
||||
created = resp.json()
|
||||
assert created["subject"] == "Hello recruiter"
|
||||
assert created["job_id"] == 1
|
||||
|
||||
resp = client.get("/api/messages", params={"job_id": 1})
|
||||
assert resp.status_code == 200
|
||||
messages = resp.json()
|
||||
assert any(m["id"] == created["id"] for m in messages)
|
||||
|
||||
|
||||
def test_delete_message(client):
|
||||
"""DELETE removes the message; subsequent GET no longer returns it."""
|
||||
resp = client.post("/api/messages", json={"type": "email", "direction": "outbound", "body": "bye"})
|
||||
assert resp.status_code == 200
|
||||
msg_id = resp.json()["id"]
|
||||
|
||||
resp = client.delete(f"/api/messages/{msg_id}")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["ok"] is True
|
||||
|
||||
resp = client.get("/api/messages")
|
||||
assert resp.status_code == 200
|
||||
ids = [m["id"] for m in resp.json()]
|
||||
assert msg_id not in ids
|
||||
|
||||
|
||||
def test_delete_message_not_found(client):
|
||||
"""DELETE /api/messages/9999 returns 404."""
|
||||
resp = client.delete("/api/messages/9999")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Templates
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_list_templates_has_builtins(client):
|
||||
"""GET /api/message-templates includes the seeded built-in keys."""
|
||||
resp = client.get("/api/message-templates")
|
||||
assert resp.status_code == 200
|
||||
templates = resp.json()
|
||||
keys = {t["key"] for t in templates}
|
||||
assert "follow_up" in keys
|
||||
assert "thank_you" in keys
|
||||
|
||||
|
||||
def test_template_create_update_delete(client):
|
||||
"""Full lifecycle: create → update title → delete a user-defined template."""
|
||||
# Create
|
||||
resp = client.post("/api/message-templates", json={
|
||||
"title": "My Template",
|
||||
"category": "custom",
|
||||
"body_template": "Hello {{name}}",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
tmpl = resp.json()
|
||||
assert tmpl["title"] == "My Template"
|
||||
assert tmpl["is_builtin"] == 0
|
||||
tmpl_id = tmpl["id"]
|
||||
|
||||
# Update title
|
||||
resp = client.put(f"/api/message-templates/{tmpl_id}", json={"title": "Updated Title"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["title"] == "Updated Title"
|
||||
|
||||
# Delete
|
||||
resp = client.delete(f"/api/message-templates/{tmpl_id}")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["ok"] is True
|
||||
|
||||
# Confirm gone
|
||||
resp = client.get("/api/message-templates")
|
||||
ids = [t["id"] for t in resp.json()]
|
||||
assert tmpl_id not in ids
|
||||
|
||||
|
||||
def test_builtin_template_put_returns_403(client):
|
||||
"""PUT on a built-in template returns 403."""
|
||||
resp = client.get("/api/message-templates")
|
||||
builtin = next(t for t in resp.json() if t["is_builtin"] == 1)
|
||||
resp = client.put(f"/api/message-templates/{builtin['id']}", json={"title": "Hacked"})
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
def test_builtin_template_delete_returns_403(client):
|
||||
"""DELETE on a built-in template returns 403."""
|
||||
resp = client.get("/api/message-templates")
|
||||
builtin = next(t for t in resp.json() if t["is_builtin"] == 1)
|
||||
resp = client.delete(f"/api/message-templates/{builtin['id']}")
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Draft reply (tier gate)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_draft_without_llm_returns_402(fresh_db, monkeypatch):
|
||||
"""POST /api/contacts/{id}/draft-reply with free tier + no LLM configured returns 402."""
|
||||
import sqlite3
|
||||
import dev_api
|
||||
|
||||
# Insert a job_contacts row so the contact_id exists
|
||||
con = sqlite3.connect(fresh_db)
|
||||
con.execute(
|
||||
"INSERT INTO job_contacts (job_id, direction, subject, from_addr, body) "
|
||||
"VALUES (NULL, 'inbound', 'Test subject', 'hr@example.com', 'We would like to schedule...')"
|
||||
)
|
||||
con.commit()
|
||||
contact_id = con.execute("SELECT last_insert_rowid()").fetchone()[0]
|
||||
con.close()
|
||||
|
||||
# Ensure has_configured_llm returns False at both import locations
|
||||
monkeypatch.setattr("app.wizard.tiers.has_configured_llm", lambda *a, **kw: False)
|
||||
|
||||
client = TestClient(dev_api.app)
|
||||
resp = client.post(
|
||||
f"/api/contacts/{contact_id}/draft-reply",
|
||||
headers={"X-CF-Tier": "free"},
|
||||
)
|
||||
assert resp.status_code == 402
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Approve
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_approve_message(client):
|
||||
"""POST /api/messages then POST /api/messages/{id}/approve returns body + approved_at."""
|
||||
resp = client.post("/api/messages", json={
|
||||
"type": "draft",
|
||||
"direction": "outbound",
|
||||
"body": "This is my draft reply.",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
msg_id = resp.json()["id"]
|
||||
assert resp.json()["approved_at"] is None
|
||||
|
||||
resp = client.post(f"/api/messages/{msg_id}/approve")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["body"] == "This is my draft reply."
|
||||
assert data["approved_at"] is not None
|
||||
|
||||
|
||||
def test_approve_message_not_found(client):
|
||||
"""POST /api/messages/9999/approve returns 404."""
|
||||
resp = client.post("/api/messages/9999/approve")
|
||||
assert resp.status_code == 404
|
||||
Loading…
Reference in a new issue