Replace team_id with account_id across all API endpoints (trees, categories, tags, steps, step_categories, admin, auth). Add new accounts and webhooks endpoints. Registration now atomically creates Account + Subscription, with account_invite_code bypassing the platform invite gate. Schemas updated for account_id/account_role. 82 tests passing including 18 new tests for accounts, subscriptions, and permissions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
161 lines
5.6 KiB
Python
161 lines
5.6 KiB
Python
"""Integration tests for authentication endpoints."""
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
|
|
class TestAuthentication:
|
|
"""Test suite for authentication endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register_user(self, client: AsyncClient):
|
|
"""Test user registration."""
|
|
user_data = {
|
|
"email": "newuser@example.com",
|
|
"password": "SecurePass123!",
|
|
"name": "New User"
|
|
}
|
|
|
|
response = await client.post("/api/v1/auth/register", json=user_data)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["email"] == user_data["email"]
|
|
assert data["name"] == user_data["name"]
|
|
assert data["role"] == "engineer"
|
|
assert "account_id" in data
|
|
assert data["account_role"] == "owner"
|
|
assert "id" in data
|
|
assert "password" not in data # Password should not be returned
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register_duplicate_email(
|
|
self, client: AsyncClient, test_user: dict
|
|
):
|
|
"""Test that registering with duplicate email fails."""
|
|
user_data = {
|
|
"email": test_user["email"], # Use existing email
|
|
"password": "AnotherPass123!",
|
|
"name": "Another User"
|
|
}
|
|
|
|
response = await client.post("/api/v1/auth/register", json=user_data)
|
|
|
|
assert response.status_code == 400
|
|
assert "already registered" in response.json()["detail"].lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_json(self, client: AsyncClient, test_user: dict):
|
|
"""Test JSON login endpoint."""
|
|
login_data = {
|
|
"email": test_user["email"],
|
|
"password": test_user["password"]
|
|
}
|
|
|
|
response = await client.post("/api/v1/auth/login/json", json=login_data)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "access_token" in data
|
|
assert "refresh_token" in data
|
|
assert data["token_type"] == "bearer"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_invalid_credentials(
|
|
self, client: AsyncClient, test_user: dict
|
|
):
|
|
"""Test login with wrong password."""
|
|
login_data = {
|
|
"email": test_user["email"],
|
|
"password": "WrongPassword123!"
|
|
}
|
|
|
|
response = await client.post("/api/v1/auth/login/json", json=login_data)
|
|
|
|
assert response.status_code == 401
|
|
assert "incorrect" in response.json()["detail"].lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_current_user(
|
|
self, client: AsyncClient, auth_headers: dict, test_user: dict
|
|
):
|
|
"""Test getting current authenticated user."""
|
|
response = await client.get("/api/v1/auth/me", headers=auth_headers)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["email"] == test_user["email"]
|
|
assert "password" not in data
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_current_user_unauthorized(self, client: AsyncClient):
|
|
"""Test that unauthenticated request fails."""
|
|
response = await client.get("/api/v1/auth/me")
|
|
|
|
assert response.status_code == 401
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register_with_role_field_ignored(self, client: AsyncClient):
|
|
"""Test that sending a role field at registration is ignored — always engineer."""
|
|
user_data = {
|
|
"email": "hacker@example.com",
|
|
"password": "HackerPass123!",
|
|
"name": "Hacker",
|
|
"role": "admin"
|
|
}
|
|
|
|
response = await client.post("/api/v1/auth/register", json=user_data)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["role"] == "engineer"
|
|
assert data["account_role"] == "owner"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register_default_role_is_engineer(self, client: AsyncClient):
|
|
"""Test that omitting role defaults to engineer."""
|
|
user_data = {
|
|
"email": "default@example.com",
|
|
"password": "DefaultPass123!",
|
|
"name": "Default User"
|
|
}
|
|
|
|
response = await client.post("/api/v1/auth/register", json=user_data)
|
|
|
|
assert response.status_code == 201
|
|
assert response.json()["role"] == "engineer"
|
|
assert response.json()["account_role"] == "owner"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register_rejects_no_uppercase(self, client: AsyncClient):
|
|
"""Test that password without uppercase is rejected."""
|
|
user_data = {
|
|
"email": "weak1@example.com",
|
|
"password": "alllowercase123",
|
|
"name": "Weak User"
|
|
}
|
|
response = await client.post("/api/v1/auth/register", json=user_data)
|
|
assert response.status_code == 422
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register_rejects_no_lowercase(self, client: AsyncClient):
|
|
"""Test that password without lowercase is rejected."""
|
|
user_data = {
|
|
"email": "weak2@example.com",
|
|
"password": "ALLUPPERCASE123",
|
|
"name": "Weak User"
|
|
}
|
|
response = await client.post("/api/v1/auth/register", json=user_data)
|
|
assert response.status_code == 422
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register_rejects_no_digit(self, client: AsyncClient):
|
|
"""Test that password without digit is rejected."""
|
|
user_data = {
|
|
"email": "weak3@example.com",
|
|
"password": "NoDigitsHere!!",
|
|
"name": "Weak User"
|
|
}
|
|
response = await client.post("/api/v1/auth/register", json=user_data)
|
|
assert response.status_code == 422
|