feat(l1): PATCH /accounts/me/members/{id}/coverage for engineer L1-coverage flag
Owner-only endpoint to toggle can_cover_l1 on an engineer user. 422 if target role is not engineer (owners/super_admins already see L1 surface; viewers/ l1_techs don't need this flag). 404 for cross-account targets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -455,3 +455,110 @@ async def test_get_seats_blocked_for_viewer(client: AsyncClient, test_db: AsyncS
|
||||
|
||||
resp = await client.get("/api/v1/accounts/me/seats", headers=viewer_headers)
|
||||
assert resp.status_code == 403, resp.text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PATCH /me/members/{user_id}/coverage — engineer L1-coverage flag
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_coverage_owner_can_toggle_engineer(client: AsyncClient, test_db: AsyncSession):
|
||||
"""Owner can set can_cover_l1=True on an engineer; response reflects new value."""
|
||||
owner = await _register(client, email="owner_cov1@example.com")
|
||||
account_id = uuid.UUID(owner["account_id"])
|
||||
headers = await _login(client, email="owner_cov1@example.com")
|
||||
|
||||
engineer = await _add_member(test_db, account_id, role="engineer", suffix="cov1")
|
||||
|
||||
resp = await client.patch(
|
||||
f"/api/v1/accounts/me/members/{engineer.id}/coverage",
|
||||
json={"can_cover_l1": True},
|
||||
headers=headers,
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
body = resp.json()
|
||||
assert body["can_cover_l1"] is True
|
||||
|
||||
# Toggle back to False
|
||||
resp2 = await client.patch(
|
||||
f"/api/v1/accounts/me/members/{engineer.id}/coverage",
|
||||
json={"can_cover_l1": False},
|
||||
headers=headers,
|
||||
)
|
||||
assert resp2.status_code == 200, resp2.text
|
||||
assert resp2.json()["can_cover_l1"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_coverage_non_owner_is_forbidden(client: AsyncClient, test_db: AsyncSession):
|
||||
"""A non-owner engineer cannot toggle coverage on themselves or others."""
|
||||
from app.core.security import get_password_hash
|
||||
|
||||
owner = await _register(client, email="owner_cov2@example.com")
|
||||
account_id = uuid.UUID(owner["account_id"])
|
||||
|
||||
# Create an engineer with a known password
|
||||
eng_password = "EngPass123!"
|
||||
engineer = User(
|
||||
id=uuid.uuid4(),
|
||||
email="eng_cov2@example.com",
|
||||
name="Eng Cov2",
|
||||
account_id=account_id,
|
||||
account_role="engineer",
|
||||
role="engineer",
|
||||
is_active=True,
|
||||
password_hash=get_password_hash(eng_password),
|
||||
)
|
||||
test_db.add(engineer)
|
||||
await test_db.commit()
|
||||
|
||||
eng_headers = await _login(client, email="eng_cov2@example.com", password=eng_password)
|
||||
|
||||
resp = await client.patch(
|
||||
f"/api/v1/accounts/me/members/{engineer.id}/coverage",
|
||||
json={"can_cover_l1": True},
|
||||
headers=eng_headers,
|
||||
)
|
||||
assert resp.status_code == 403, resp.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_coverage_viewer_role_returns_422(client: AsyncClient, test_db: AsyncSession):
|
||||
"""PATCH coverage on a viewer → 422 (coverage flag only applies to engineers)."""
|
||||
owner = await _register(client, email="owner_cov3@example.com")
|
||||
account_id = uuid.UUID(owner["account_id"])
|
||||
headers = await _login(client, email="owner_cov3@example.com")
|
||||
|
||||
viewer = await _add_member(test_db, account_id, role="viewer", suffix="cov3")
|
||||
|
||||
resp = await client.patch(
|
||||
f"/api/v1/accounts/me/members/{viewer.id}/coverage",
|
||||
json={"can_cover_l1": True},
|
||||
headers=headers,
|
||||
)
|
||||
assert resp.status_code == 422, resp.text
|
||||
assert "engineer" in resp.json()["detail"].lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_coverage_cross_account_returns_404(client: AsyncClient, test_db: AsyncSession):
|
||||
"""PATCH coverage on a user from a different account → 404 (tenancy isolation)."""
|
||||
# Account A
|
||||
owner_a = await _register(client, email="owner_cov_a@example.com")
|
||||
account_a_id = uuid.UUID(owner_a["account_id"])
|
||||
headers_a = await _login(client, email="owner_cov_a@example.com")
|
||||
|
||||
# Account B — a separate registration creates a new account
|
||||
owner_b = await _register(client, email="owner_cov_b@example.com")
|
||||
account_b_id = uuid.UUID(owner_b["account_id"])
|
||||
|
||||
# Add an engineer to account B
|
||||
engineer_b = await _add_member(test_db, account_b_id, role="engineer", suffix="covb")
|
||||
|
||||
# Owner of account A tries to patch account B's engineer — must 404
|
||||
resp = await client.patch(
|
||||
f"/api/v1/accounts/me/members/{engineer_b.id}/coverage",
|
||||
json={"can_cover_l1": True},
|
||||
headers=headers_a,
|
||||
)
|
||||
assert resp.status_code == 404, resp.text
|
||||
|
||||
Reference in New Issue
Block a user