Files
crm-system/tests/test_db_write_cycles.py

239 lines
9.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""DB write-cycle tests: CRUD for every business entity.
Each test creates a fresh record via the API, verifies it via the list endpoint,
updates one field, re-verifies, then soft-deletes.
Uses the existing conftest.py fixtures (client, registered_user, seed_data).
No real HTTP connection async ASGITransport.
"""
from __future__ import annotations
from typing import Any
import pytest
from httpx import AsyncClient
# =========================================================
# Helper
# =========================================================
async def _crud_cycle(
client: AsyncClient,
auth_headers: dict[str, str],
entity: str,
create_payload: dict[str, Any],
update_payload: dict[str, Any],
read_field: str,
skip_update: bool = False,
) -> dict[str, Any]:
"""Run create -> verify-in-list -> update -> verify-in-list -> delete."""
list_path = f"/api/v1/{entity}/"
detail_base = f"/api/v1/{entity}/"
# ---------- CREATE ----------
create_resp = await client.post(list_path, json=create_payload, headers=auth_headers)
assert create_resp.status_code == 201, f"CREATE {entity}: {create_resp.status_code} {create_resp.text}"
created = create_resp.json()
obj_id = created.get("id")
assert obj_id is not None, f"CREATE {entity} returned no id"
# ---------- VERIFY IN LIST ----------
list_resp = await client.get(list_path, headers=auth_headers)
assert list_resp.status_code == 200, f"LIST {entity}: {list_resp.status_code}"
items = list_resp.json()
if isinstance(items, dict):
items = items.get("items", [])
matching = [i for i in items if i.get("id") == obj_id]
assert len(matching) == 1, f"LIST {entity}: object with id {obj_id} not found in list"
assert matching[0].get(read_field) == create_payload[read_field], \
f"LIST {entity}: field '{read_field}' mismatch"
# ---------- UPDATE ----------
if not skip_update:
patch_path = f"{detail_base}{obj_id}"
patch_resp = await client.patch(patch_path, json=update_payload, headers=auth_headers)
assert patch_resp.status_code == 200, f"PATCH {entity} {obj_id}: {patch_resp.status_code} {patch_resp.text}"
patched = patch_resp.json()
update_field = next(iter(update_payload))
assert patched[update_field] == update_payload[update_field], \
f"PATCH result mismatch for {entity}"
# Verify update persisted via list
list_after = await client.get(list_path, headers=auth_headers)
items_after = list_after.json()
if isinstance(items_after, dict):
items_after = items_after.get("items", [])
updated_match = [i for i in items_after if i.get("id") == obj_id]
assert updated_match, f"Object {obj_id} still missing from list after PATCH"
# ---------- DELETE ----------
delete_resp = await client.delete(f"{detail_base}{obj_id}", headers=auth_headers)
assert delete_resp.status_code == 204, f"DELETE {entity} {obj_id}: {delete_resp.status_code}"
# Verify deleted from list
list_deleted = await client.get(list_path, headers=auth_headers)
items_deleted = list_deleted.json()
if isinstance(items_deleted, dict):
items_deleted = items_deleted.get("items", [])
deleted_match = [i for i in items_deleted if i.get("id") == obj_id]
assert len(deleted_match) == 0, f"Object {obj_id} still appears in list after DELETE"
return created
# =========================================================
# 1. Users CRUD
# =========================================================
@pytest.mark.asyncio
async def test_user_crud(client: AsyncClient, registered_user: dict[str, Any]) -> None:
"""Create user → verify in list → update → delete."""
headers = registered_user["headers"]
await _crud_cycle(
client, headers,
entity="users",
create_payload={"email": "crud_user@test.com", "password": "Test1234!", "name": "CRUD User", "role": "sales_rep"},
update_payload={"name": "Updated CRUD User"},
read_field="name",
)
# =========================================================
# 2. Accounts CRUD
# =========================================================
@pytest.mark.asyncio
async def test_account_crud(client: AsyncClient, registered_user: dict[str, Any]) -> None:
"""Create account → verify in list → update → delete."""
headers = registered_user["headers"]
await _crud_cycle(
client, headers,
entity="accounts",
create_payload={"name": "CRUD Account", "industry": "sme"},
update_payload={"name": "Updated Account"},
read_field="name",
)
# =========================================================
# 3. Contacts CRUD
# =========================================================
@pytest.mark.asyncio
async def test_contact_crud(client: AsyncClient, registered_user: dict[str, Any]) -> None:
"""Create contact → verify in list → update → delete."""
headers = registered_user["headers"]
await _crud_cycle(
client, headers,
entity="contacts",
create_payload={"first_name": "Jane", "last_name": "Doe"},
update_payload={"first_name": "Janet"},
read_field="first_name",
)
# =========================================================
# 4. Deals CRUD (needs an account_id)
# =========================================================
@pytest.mark.asyncio
async def test_deal_crud(client: AsyncClient, seed_data: dict[str, Any]) -> None:
"""Create deal → verify in list → update → delete."""
headers = seed_data["headers"]
account_id = seed_data["account_ids"][0]
await _crud_cycle(
client, headers,
entity="deals",
create_payload={"title": "CRUD Deal", "account_id": account_id},
update_payload={"title": "Updated Deal"},
read_field="title",
)
# =========================================================
# 5. Activities CRUD
# =========================================================
@pytest.mark.asyncio
async def test_activity_crud(client: AsyncClient, seed_data: dict[str, Any]) -> None:
"""Create activity → verify in list → update → delete."""
headers = seed_data["headers"]
account_id = seed_data["account_ids"][0]
await _crud_cycle(
client, headers,
entity="activities",
create_payload={"type": "task", "subject": "CRUD Activity", "account_id": account_id},
update_payload={"subject": "Updated Activity"},
read_field="subject",
)
# =========================================================
# 6. Notes CRUD (needs parent_type/parent_id)
# =========================================================
@pytest.mark.asyncio
async def test_note_crud(client: AsyncClient, seed_data: dict[str, Any]) -> None:
"""Create note → verify in list → update → delete."""
headers = seed_data["headers"]
account_id = seed_data["account_ids"][0]
await _crud_cycle(
client, headers,
entity="notes",
create_payload={"body": "CRUD Note", "parent_type": "account", "parent_id": account_id},
update_payload={"body": "Updated Note"},
read_field="body",
)
# =========================================================
# 7. Tags CRUD (no GET-by-ID, no PATCH)
# =========================================================
@pytest.mark.asyncio
async def test_tag_crud(client: AsyncClient, registered_user: dict[str, Any]) -> None:
"""Create tag → verify in list. (No DELETE endpoint for single tags; unlink is via /tags/link)."""
headers = registered_user["headers"]
list_path = "/api/v1/tags/"
create_payload = {"name": "CRUD Tag", "color": "#FFCC00"}
# CREATE
create_resp = await client.post(list_path, json=create_payload, headers=headers)
assert create_resp.status_code == 201, f"CREATE tags: {create_resp.status_code} {create_resp.text}"
created = create_resp.json()
obj_id = created.get("id")
assert obj_id is not None
# VERIFY IN LIST
list_resp = await client.get(list_path, headers=headers)
assert list_resp.status_code == 200
items = list_resp.json()
matching = [i for i in items if i.get("id") == obj_id]
assert len(matching) == 1
assert matching[0].get("name") == create_payload["name"]
# =========================================================
# 8. Org only GET + PATCH exist, but neither is implemented
# =========================================================
@pytest.mark.asyncio
async def test_org_endpoint_not_implemented(
client: AsyncClient, registered_user: dict[str, Any]
) -> None:
"""Verify /api/v1/org returns 404 (endpoint not yet implemented)."""
headers = registered_user["headers"]
get_resp = await client.get("/api/v1/org", headers=headers)
assert get_resp.status_code == 404, \
f"GET /api/v1/org → {get_resp.status_code} (expected 404, endpoint not implemented in Phase 4a-4d)"
patch_resp = await client.patch(
"/api/v1/org",
json={"name": "Test"},
headers=headers,
)
assert patch_resp.status_code in (404, 405), \
f"PATCH /api/v1/org → {patch_resp.status_code} (expected 404 or 405)"