240 lines
9.0 KiB
Python
240 lines
9.0 KiB
Python
|
|
"""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)"
|