feat(l1): GET /accounts/me/seats endpoint for seat counter widget
Returns {engineer: SeatCheckResult, l1_tech: SeatCheckResult} for the
authenticated engineer's account. Powers the SeatCounterWidget UI in the
admin/users + account/users surfaces. Engineer+ access only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -394,3 +394,64 @@ async def test_role_change_demotion_bypasses_seat_check(client: AsyncClient, tes
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert resp.json()["account_role"] == "viewer"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /me/seats — seat counter widget endpoint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_seats_returns_both_role_counts(client: AsyncClient, test_db: AsyncSession):
|
||||
"""GET /accounts/me/seats returns engineer + l1_tech seat usage."""
|
||||
owner = await _register(client, email="owner_seats@example.com")
|
||||
account_id = uuid.UUID(owner["account_id"])
|
||||
headers = await _login(client, email="owner_seats@example.com")
|
||||
await _set_sub(test_db, account_id, seat_limit=5, l1_seat_limit=3)
|
||||
# Add 2 engineers and 1 l1_tech as members
|
||||
for i in range(2):
|
||||
await _add_member(test_db, account_id, role="engineer", suffix=f"e{i}")
|
||||
await _add_member(test_db, account_id, role="l1_tech", suffix="l1")
|
||||
|
||||
resp = await client.get("/api/v1/accounts/me/seats", headers=headers)
|
||||
assert resp.status_code == 200, resp.text
|
||||
body = resp.json()
|
||||
assert body["engineer"]["role"] == "engineer"
|
||||
assert body["engineer"]["current"] == 2
|
||||
assert body["engineer"]["limit"] == 5
|
||||
assert body["engineer"]["available"] is True
|
||||
assert body["l1_tech"]["role"] == "l1_tech"
|
||||
assert body["l1_tech"]["current"] == 1
|
||||
assert body["l1_tech"]["limit"] == 3
|
||||
assert body["l1_tech"]["available"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_seats_blocked_for_viewer(client: AsyncClient, test_db: AsyncSession):
|
||||
"""GET /accounts/me/seats → 403 for viewer role (engineer+ required)."""
|
||||
from app.core.security import get_password_hash
|
||||
|
||||
# Register an owner for the account
|
||||
owner = await _register(client, email="owner_seats2@example.com")
|
||||
account_id = uuid.UUID(owner["account_id"])
|
||||
await _set_sub(test_db, account_id, seat_limit=5, l1_seat_limit=3)
|
||||
|
||||
# Create a viewer user with a known password directly in the DB
|
||||
viewer_password = "ViewerPass123!"
|
||||
viewer = User(
|
||||
id=uuid.uuid4(),
|
||||
email="viewer_seats@example.com",
|
||||
name="Viewer Seats",
|
||||
account_id=account_id,
|
||||
account_role="viewer",
|
||||
role="engineer", # system role field (default)
|
||||
is_active=True,
|
||||
password_hash=get_password_hash(viewer_password),
|
||||
)
|
||||
test_db.add(viewer)
|
||||
await test_db.commit()
|
||||
|
||||
# Log in as the viewer
|
||||
viewer_headers = await _login(client, email="viewer_seats@example.com", password=viewer_password)
|
||||
|
||||
resp = await client.get("/api/v1/accounts/me/seats", headers=viewer_headers)
|
||||
assert resp.status_code == 403, resp.text
|
||||
|
||||
Reference in New Issue
Block a user