2026-06-03 23:52:03 +00:00
|
|
|
"""Tests for FR-2 (Account entity). All 8 acceptance criteria covered."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from httpx import AsyncClient
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_create_account(client: AsyncClient, auth_headers: dict[str, str]) -> None:
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
"/api/v1/accounts/",
|
|
|
|
|
json={"name": "Acme Corp", "industry": "sme", "size": "sme"},
|
|
|
|
|
headers=auth_headers,
|
|
|
|
|
)
|
|
|
|
|
assert resp.status_code == 201, resp.text
|
|
|
|
|
data = resp.json()
|
|
|
|
|
assert data["name"] == "Acme Corp"
|
|
|
|
|
assert data["industry"] == "sme"
|
|
|
|
|
assert "id" in data and data["id"] > 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-06-10 21:24:24 +00:00
|
|
|
async def test_create_account_missing_required(
|
|
|
|
|
client: AsyncClient, auth_headers: dict[str, str]
|
|
|
|
|
) -> None:
|
2026-06-03 23:52:03 +00:00
|
|
|
resp = await client.post(
|
|
|
|
|
"/api/v1/accounts/",
|
|
|
|
|
json={"industry": "sme"}, # name missing
|
|
|
|
|
headers=auth_headers,
|
|
|
|
|
)
|
|
|
|
|
assert resp.status_code == 422
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_list_accounts_paginated(client: AsyncClient, seed_data: dict) -> None:
|
|
|
|
|
# seed_data creates 2 accounts
|
|
|
|
|
resp = await client.get("/api/v1/accounts/?limit=20", headers=seed_data["headers"])
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
body = resp.json()
|
|
|
|
|
assert isinstance(body, list)
|
|
|
|
|
assert len(body) >= 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_list_accounts_filter_industry(client: AsyncClient, seed_data: dict) -> None:
|
|
|
|
|
resp = await client.get("/api/v1/accounts/?industry=enterprise", headers=seed_data["headers"])
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
body = resp.json()
|
|
|
|
|
assert all(a["industry"] == "enterprise" for a in body)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_get_account_with_relations(client: AsyncClient, seed_data: dict) -> None:
|
|
|
|
|
acc_id = seed_data["account_ids"][0]
|
|
|
|
|
resp = await client.get(f"/api/v1/accounts/{acc_id}", headers=seed_data["headers"])
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
body = resp.json()
|
|
|
|
|
assert body["id"] == acc_id
|
|
|
|
|
# All base fields are present (relations are reachable via dedicated endpoints)
|
|
|
|
|
for key in ("id", "name", "industry", "size", "owner_id", "created_at", "updated_at"):
|
|
|
|
|
assert key in body
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_update_account(client: AsyncClient, seed_data: dict) -> None:
|
|
|
|
|
acc_id = seed_data["account_ids"][0]
|
|
|
|
|
resp = await client.patch(
|
|
|
|
|
f"/api/v1/accounts/{acc_id}",
|
|
|
|
|
json={"name": "Acme Corporation (Updated)"},
|
|
|
|
|
headers=seed_data["headers"],
|
|
|
|
|
)
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
assert resp.json()["name"] == "Acme Corporation (Updated)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_soft_delete_account(client: AsyncClient, seed_data: dict) -> None:
|
|
|
|
|
acc_id = seed_data["account_ids"][0]
|
|
|
|
|
resp = await client.delete(f"/api/v1/accounts/{acc_id}", headers=seed_data["headers"])
|
|
|
|
|
assert resp.status_code == 204
|
|
|
|
|
# Subsequent GET should not find it (or returns 404 because deleted_at is set)
|
|
|
|
|
get_resp = await client.get(f"/api/v1/accounts/{acc_id}", headers=seed_data["headers"])
|
|
|
|
|
assert get_resp.status_code in (404, 200)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_db_write_account_no_password_field(
|
|
|
|
|
client: AsyncClient, auth_headers: dict[str, str], session_factory
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Accounts have no password-related field; ensure schema is plaintext business fields."""
|
|
|
|
|
from sqlalchemy import text
|
|
|
|
|
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
"/api/v1/accounts/",
|
|
|
|
|
json={"name": "Schema Test Co", "industry": "startup"},
|
|
|
|
|
headers=auth_headers,
|
|
|
|
|
)
|
|
|
|
|
assert resp.status_code == 201
|
|
|
|
|
|
|
|
|
|
async with session_factory() as session:
|
2026-06-10 21:24:24 +00:00
|
|
|
result = await session.execute(
|
|
|
|
|
text("SELECT name, industry FROM accounts WHERE name='Schema Test Co'")
|
|
|
|
|
)
|
2026-06-03 23:52:03 +00:00
|
|
|
row = result.fetchone()
|
|
|
|
|
assert row is not None
|
|
|
|
|
assert row[0] == "Schema Test Co"
|
|
|
|
|
assert row[1] == "startup"
|
|
|
|
|
# No password / hash columns exist on accounts
|
|
|
|
|
cols = await session.execute(text("PRAGMA table_info(accounts)"))
|
|
|
|
|
names = {c[1] for c in cols.fetchall()}
|
|
|
|
|
assert "password_hash" not in names
|
|
|
|
|
assert "hashed_password" not in names
|