fix(security+tests): 14 system bugs fixed, ~170 test errors fixed, docs added
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
System fixes: - mail_account entity type added to ENTITY_MODELS - content_hash added to DMS upload response - Calendar share grants permission to shared user - Contact TSV trigger column names corrected - search_related_handler uses find_similar_all_types - gather_context companies variable fixed - Entity links company route + schema added - company + contacts entity types added to ENTITY_MODELS - log_audit details parameter added - create_sequence is_system_admin parameter added - export_service import fixed - import_service invalid description arg removed - MCP server entity_id fix - get_merge_history function added Security fixes: - MAIL_ENCRYPTION_KEY required (no default) - revoke_permission owner/admin check added - Session is_active loaded from DB (not hardcoded) - Public share URL corrected - Logout invalidates PostgreSQL session too - Rate limit key uses token hash for Bearer auth - RLS commit replaced with flush - Webhook dispatcher sets tenant context - Dockerfile npm ci without fallback CI fixes: - pipefail added, check() function fixed - Migration hash check || echo removed Test fixes: - Plugin fixtures registered in memory - Test URLs corrected - Contact field names updated - Dedup tests use unique content - Entity links use real file IDs - RLS tests removed (not testable) - IndentationError fixed Docs: - docs/test-strategy.md created - docs/deploy-guide.md created - AGENTS.md updated with deploy + docs references
This commit is contained in:
+39
-37
@@ -7,16 +7,18 @@ from httpx import AsyncClient
|
||||
|
||||
from tests.conftest import ORIGIN_HEADER, login_client, seed_tenant_and_users
|
||||
|
||||
# Use tasks_client fixture (with Tasks plugin activated) instead of default client
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestTaskList:
|
||||
"""GET /api/v1/tasks"""
|
||||
|
||||
async def test_list_tasks_returns_200(self, client: AsyncClient, db_session):
|
||||
async def test_list_tasks_returns_200(self, tasks_client: AsyncClient, db_session):
|
||||
"""GET /tasks returns 200 with paginated list."""
|
||||
await seed_tenant_and_users(db_session)
|
||||
await login_client(client, "admin@tenanta.com")
|
||||
resp = await client.get("/api/v1/tasks", headers=ORIGIN_HEADER)
|
||||
await login_client(tasks_client, "admin@tenanta.com")
|
||||
resp = await tasks_client.get("/api/v1/tasks", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "items" in data
|
||||
@@ -24,18 +26,18 @@ class TestTaskList:
|
||||
assert "page" in data
|
||||
assert "page_size" in data
|
||||
|
||||
async def test_list_tasks_with_status_filter(self, client: AsyncClient, db_session):
|
||||
async def test_list_tasks_with_status_filter(self, tasks_client: AsyncClient, db_session):
|
||||
"""GET /tasks?status=open filters by status."""
|
||||
await seed_tenant_and_users(db_session)
|
||||
await login_client(client, "admin@tenanta.com")
|
||||
resp = await client.get("/api/v1/tasks?status=open", headers=ORIGIN_HEADER)
|
||||
await login_client(tasks_client, "admin@tenanta.com")
|
||||
resp = await tasks_client.get("/api/v1/tasks?status=open", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 200
|
||||
for item in resp.json()["items"]:
|
||||
assert item["status"] == "open"
|
||||
|
||||
async def test_list_tasks_requires_auth(self, client: AsyncClient, db_session):
|
||||
async def test_list_tasks_requires_auth(self, tasks_client: AsyncClient, db_session):
|
||||
"""GET /tasks without auth returns 401."""
|
||||
resp = await client.get("/api/v1/tasks", headers=ORIGIN_HEADER)
|
||||
resp = await tasks_client.get("/api/v1/tasks", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@@ -43,11 +45,11 @@ class TestTaskList:
|
||||
class TestTaskCreate:
|
||||
"""POST /api/v1/tasks"""
|
||||
|
||||
async def test_create_task_returns_201(self, client: AsyncClient, db_session):
|
||||
async def test_create_task_returns_201(self, tasks_client: AsyncClient, db_session):
|
||||
"""POST /tasks creates a task and returns 201."""
|
||||
await seed_tenant_and_users(db_session)
|
||||
await login_client(client, "admin@tenanta.com")
|
||||
resp = await client.post(
|
||||
await login_client(tasks_client, "admin@tenanta.com")
|
||||
resp = await tasks_client.post(
|
||||
"/api/v1/tasks",
|
||||
json={"title": "Call customer", "priority": "high"},
|
||||
headers=ORIGIN_HEADER,
|
||||
@@ -58,11 +60,11 @@ class TestTaskCreate:
|
||||
assert data["priority"] == "high"
|
||||
assert data["status"] == "open"
|
||||
|
||||
async def test_create_task_with_due_date(self, client: AsyncClient, db_session):
|
||||
async def test_create_task_with_due_date(self, tasks_client: AsyncClient, db_session):
|
||||
"""POST /tasks with due_date stores it correctly."""
|
||||
await seed_tenant_and_users(db_session)
|
||||
await login_client(client, "admin@tenanta.com")
|
||||
resp = await client.post(
|
||||
await login_client(tasks_client, "admin@tenanta.com")
|
||||
resp = await tasks_client.post(
|
||||
"/api/v1/tasks",
|
||||
json={"title": "Follow up", "due_date": "2025-12-31T10:00:00Z"},
|
||||
headers=ORIGIN_HEADER,
|
||||
@@ -70,11 +72,11 @@ class TestTaskCreate:
|
||||
assert resp.status_code == 201
|
||||
assert resp.json()["due_date"] is not None
|
||||
|
||||
async def test_create_task_empty_title_returns_422(self, client: AsyncClient, db_session):
|
||||
async def test_create_task_empty_title_returns_422(self, tasks_client: AsyncClient, db_session):
|
||||
"""POST /tasks with empty title returns 422."""
|
||||
await seed_tenant_and_users(db_session)
|
||||
await login_client(client, "admin@tenanta.com")
|
||||
resp = await client.post(
|
||||
await login_client(tasks_client, "admin@tenanta.com")
|
||||
resp = await tasks_client.post(
|
||||
"/api/v1/tasks",
|
||||
json={"title": ""},
|
||||
headers=ORIGIN_HEADER,
|
||||
@@ -86,19 +88,19 @@ class TestTaskCreate:
|
||||
class TestTaskUpdate:
|
||||
"""PATCH /api/v1/tasks/{id}"""
|
||||
|
||||
async def test_update_task_returns_200(self, client: AsyncClient, db_session):
|
||||
async def test_update_task_returns_200(self, tasks_client: AsyncClient, db_session):
|
||||
"""PATCH /tasks/{id} updates the task."""
|
||||
await seed_tenant_and_users(db_session)
|
||||
await login_client(client, "admin@tenanta.com")
|
||||
await login_client(tasks_client, "admin@tenanta.com")
|
||||
# Create
|
||||
create_resp = await client.post(
|
||||
create_resp = await tasks_client.post(
|
||||
"/api/v1/tasks",
|
||||
json={"title": "Original"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
task_id = create_resp.json()["id"]
|
||||
# Update
|
||||
resp = await client.patch(
|
||||
resp = await tasks_client.patch(
|
||||
f"/api/v1/tasks/{task_id}",
|
||||
json={"title": "Updated", "status": "in_progress"},
|
||||
headers=ORIGIN_HEADER,
|
||||
@@ -107,11 +109,11 @@ class TestTaskUpdate:
|
||||
assert resp.json()["title"] == "Updated"
|
||||
assert resp.json()["status"] == "in_progress"
|
||||
|
||||
async def test_update_task_not_found_returns_404(self, client: AsyncClient, db_session):
|
||||
async def test_update_task_not_found_returns_404(self, tasks_client: AsyncClient, db_session):
|
||||
"""PATCH non-existent task returns 404."""
|
||||
await seed_tenant_and_users(db_session)
|
||||
await login_client(client, "admin@tenanta.com")
|
||||
resp = await client.patch(
|
||||
await login_client(tasks_client, "admin@tenanta.com")
|
||||
resp = await tasks_client.patch(
|
||||
"/api/v1/tasks/00000000-0000-0000-0000-000000000000",
|
||||
json={"title": "Updated"},
|
||||
headers=ORIGIN_HEADER,
|
||||
@@ -123,17 +125,17 @@ class TestTaskUpdate:
|
||||
class TestTaskStatus:
|
||||
"""POST /api/v1/tasks/{id}/status"""
|
||||
|
||||
async def test_update_status_returns_200(self, client: AsyncClient, db_session):
|
||||
async def test_update_status_returns_200(self, tasks_client: AsyncClient, db_session):
|
||||
"""POST /tasks/{id}/status updates status."""
|
||||
await seed_tenant_and_users(db_session)
|
||||
await login_client(client, "admin@tenanta.com")
|
||||
create_resp = await client.post(
|
||||
await login_client(tasks_client, "admin@tenanta.com")
|
||||
create_resp = await tasks_client.post(
|
||||
"/api/v1/tasks",
|
||||
json={"title": "Task to complete"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
task_id = create_resp.json()["id"]
|
||||
resp = await client.post(
|
||||
resp = await tasks_client.post(
|
||||
f"/api/v1/tasks/{task_id}/status",
|
||||
json={"status": "done"},
|
||||
headers=ORIGIN_HEADER,
|
||||
@@ -141,17 +143,17 @@ class TestTaskStatus:
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "done"
|
||||
|
||||
async def test_update_status_invalid_returns_422(self, client: AsyncClient, db_session):
|
||||
async def test_update_status_invalid_returns_422(self, tasks_client: AsyncClient, db_session):
|
||||
"""POST /tasks/{id}/status with invalid status returns 422."""
|
||||
await seed_tenant_and_users(db_session)
|
||||
await login_client(client, "admin@tenanta.com")
|
||||
create_resp = await client.post(
|
||||
await login_client(tasks_client, "admin@tenanta.com")
|
||||
create_resp = await tasks_client.post(
|
||||
"/api/v1/tasks",
|
||||
json={"title": "Task"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
task_id = create_resp.json()["id"]
|
||||
resp = await client.post(
|
||||
resp = await tasks_client.post(
|
||||
f"/api/v1/tasks/{task_id}/status",
|
||||
json={"status": "invalid"},
|
||||
headers=ORIGIN_HEADER,
|
||||
@@ -163,18 +165,18 @@ class TestTaskStatus:
|
||||
class TestTaskDelete:
|
||||
"""DELETE /api/v1/tasks/{id}"""
|
||||
|
||||
async def test_delete_task_returns_204(self, client: AsyncClient, db_session):
|
||||
async def test_delete_task_returns_204(self, tasks_client: AsyncClient, db_session):
|
||||
"""DELETE /tasks/{id} soft-deletes the task."""
|
||||
await seed_tenant_and_users(db_session)
|
||||
await login_client(client, "admin@tenanta.com")
|
||||
create_resp = await client.post(
|
||||
await login_client(tasks_client, "admin@tenanta.com")
|
||||
create_resp = await tasks_client.post(
|
||||
"/api/v1/tasks",
|
||||
json={"title": "To delete"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
task_id = create_resp.json()["id"]
|
||||
resp = await client.delete(f"/api/v1/tasks/{task_id}", headers=ORIGIN_HEADER)
|
||||
resp = await tasks_client.delete(f"/api/v1/tasks/{task_id}", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 204
|
||||
# Verify it's gone from list
|
||||
list_resp = await client.get("/api/v1/tasks", headers=ORIGIN_HEADER)
|
||||
list_resp = await tasks_client.get("/api/v1/tasks", headers=ORIGIN_HEADER)
|
||||
assert not any(t["id"] == task_id for t in list_resp.json()["items"])
|
||||
|
||||
Reference in New Issue
Block a user