Mounts on Pro routers (trees, sessions, scripts, FlowPilot, etc.) and returns 402 with structured detail when an account's subscription is missing or locked. Allowlist bypasses billing/account/auth flows so users can recover from a lapsed subscription. Conftest now seeds a default Pro/active Subscription on test_user and test_admin (delete-then-insert because the register endpoint already creates a free/active sub by default). Two existing tests adapted to the new seeded plan; tenant-isolation tests seed Subscription rows for the accounts they create directly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
175 lines
6.8 KiB
Python
175 lines
6.8 KiB
Python
"""Integration tests for account management endpoints."""
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
|
|
class TestAccountEndpoints:
|
|
"""Test suite for account management endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_my_account(self, client: AsyncClient, auth_headers: dict):
|
|
"""Test getting current user's account."""
|
|
response = await client.get("/api/v1/accounts/me", headers=auth_headers)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "id" in data
|
|
assert "name" in data
|
|
assert "display_code" in data
|
|
assert "owner_id" in data
|
|
assert len(data["display_code"]) == 8
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_my_subscription(self, client: AsyncClient, auth_headers: dict):
|
|
"""Test getting current user's subscription details.
|
|
|
|
The test_user fixture seeds a Pro/active Subscription so
|
|
Pro-guarded routers work; reflect that in the expected plan.
|
|
"""
|
|
response = await client.get("/api/v1/accounts/me/subscription", headers=auth_headers)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "subscription" in data
|
|
assert "limits" in data
|
|
assert "usage" in data
|
|
assert data["subscription"]["plan"] == "pro"
|
|
assert data["subscription"]["status"] == "active"
|
|
assert data["limits"]["max_trees"] == 25
|
|
assert data["limits"]["max_sessions_per_month"] == 200
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_my_members(self, client: AsyncClient, auth_headers: dict):
|
|
"""Test getting members of current user's account."""
|
|
response = await client.get("/api/v1/accounts/me/members", headers=auth_headers)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert isinstance(data, list)
|
|
assert len(data) >= 1
|
|
# Current user should be in members list
|
|
assert any(m["account_role"] == "owner" for m in data)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_my_account(self, client: AsyncClient, auth_headers: dict):
|
|
"""Test updating account name."""
|
|
response = await client.patch(
|
|
"/api/v1/accounts/me",
|
|
json={"name": "Updated Account Name"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "Updated Account Name"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_account_requires_owner(self, client: AsyncClient):
|
|
"""Test that non-owners cannot update account settings."""
|
|
# Register two users
|
|
owner_data = {
|
|
"email": "owner@example.com",
|
|
"password": "OwnerPass123!",
|
|
"name": "Owner"
|
|
}
|
|
await client.post("/api/v1/auth/register", json=owner_data)
|
|
|
|
# Login as owner and create an invite
|
|
login_resp = await client.post("/api/v1/auth/login/json", json={
|
|
"email": "owner@example.com",
|
|
"password": "OwnerPass123!"
|
|
})
|
|
owner_headers = {"Authorization": f"Bearer {login_resp.json()['access_token']}"}
|
|
|
|
# Create invite
|
|
invite_resp = await client.post(
|
|
"/api/v1/accounts/me/invites",
|
|
json={"email": "member@example.com", "role": "engineer"},
|
|
headers=owner_headers
|
|
)
|
|
assert invite_resp.status_code == 201
|
|
invite_code = invite_resp.json()["code"]
|
|
|
|
# Register member with account invite code
|
|
member_data = {
|
|
"email": "member@example.com",
|
|
"password": "MemberPass123!",
|
|
"name": "Member",
|
|
"account_invite_code": invite_code
|
|
}
|
|
reg_resp = await client.post("/api/v1/auth/register", json=member_data)
|
|
assert reg_resp.status_code == 201
|
|
assert reg_resp.json()["account_role"] == "engineer"
|
|
|
|
# Login as member
|
|
member_login = await client.post("/api/v1/auth/login/json", json={
|
|
"email": "member@example.com",
|
|
"password": "MemberPass123!"
|
|
})
|
|
member_headers = {"Authorization": f"Bearer {member_login.json()['access_token']}"}
|
|
|
|
# Member should not be able to update account
|
|
response = await client.patch(
|
|
"/api/v1/accounts/me",
|
|
json={"name": "Hacked Name"},
|
|
headers=member_headers
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_and_list_invites(self, client: AsyncClient, auth_headers: dict):
|
|
"""Test creating and listing account invites."""
|
|
# Create invite
|
|
response = await client.post(
|
|
"/api/v1/accounts/me/invites",
|
|
json={"email": "invitee@example.com", "role": "engineer"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["email"] == "invitee@example.com"
|
|
assert data["role"] == "engineer"
|
|
assert "code" in data
|
|
|
|
# List invites
|
|
list_response = await client.get("/api/v1/accounts/me/invites", headers=auth_headers)
|
|
assert list_response.status_code == 200
|
|
invites = list_response.json()
|
|
assert len(invites) >= 1
|
|
assert any(i["email"] == "invitee@example.com" for i in invites)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register_with_account_invite(self, client: AsyncClient, auth_headers: dict):
|
|
"""Test that account invite code joins user to existing account."""
|
|
# Get current account
|
|
account_resp = await client.get("/api/v1/accounts/me", headers=auth_headers)
|
|
account_id = account_resp.json()["id"]
|
|
|
|
# Create invite
|
|
invite_resp = await client.post(
|
|
"/api/v1/accounts/me/invites",
|
|
json={"email": "joiner@example.com", "role": "viewer"},
|
|
headers=auth_headers
|
|
)
|
|
invite_code = invite_resp.json()["code"]
|
|
|
|
# Register with account invite code
|
|
reg_resp = await client.post("/api/v1/auth/register", json={
|
|
"email": "joiner@example.com",
|
|
"password": "JoinerPass123!",
|
|
"name": "Joiner",
|
|
"account_invite_code": invite_code
|
|
})
|
|
assert reg_resp.status_code == 201
|
|
data = reg_resp.json()
|
|
assert data["account_id"] == account_id
|
|
assert data["account_role"] == "viewer"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register_with_invalid_invite_code(self, client: AsyncClient):
|
|
"""Test that invalid account invite code is rejected."""
|
|
response = await client.post("/api/v1/auth/register", json={
|
|
"email": "bad@example.com",
|
|
"password": "BadPassword123!",
|
|
"name": "Bad User",
|
|
"account_invite_code": "INVALID_CODE"
|
|
})
|
|
assert response.status_code == 400
|
|
assert "invalid" in response.json()["detail"].lower()
|