Initial Claude implementation

This commit is contained in:
Ian Roddis committed 2026-06-22 14:26:44 -03:00
commit e2d3ab9c2a
63 files changed
+3930

No files matched your search

View File
Whitespace-only changes.
+65
View File
@@ -0,0 +1,65 @@
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
from app.core.database import Base, get_db
from app.crud import role as role_crud
from app.crud import user as user_crud
from app.main import app
from scripts.seed import PERMISSIONS, ROLES
@pytest_asyncio.fixture
async def db_sessionmaker():
# In-memory SQLite shared across connections for the test's lifetime.
engine = create_async_engine(
"sqlite+aiosqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
maker = async_sessionmaker(engine, expire_on_commit=False)
yield maker
await engine.dispose()
@pytest_asyncio.fixture
async def seeded(db_sessionmaker):
"""Seed permissions + roles and return ids needed by tests."""
async with db_sessionmaker() as db:
perms = {}
for name, desc in PERMISSIONS:
perms[name] = await role_crud.create_permission(db, name=name, description=desc)
roles = {}
for role_name, perm_names in ROLES.items():
roles[role_name] = await role_crud.create(
db,
name=role_name,
permission_ids=[perms[p].id for p in perm_names],
)
return {"roles": roles, "perms": perms}
@pytest_asyncio.fixture
async def client(db_sessionmaker):
async def override_get_db():
async with db_sessionmaker() as session:
yield session
app.dependency_overrides[get_db] = override_get_db
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as c:
yield c
app.dependency_overrides.clear()
@pytest_asyncio.fixture
async def make_user(db_sessionmaker):
async def _make(email, password="secret123", **kwargs):
async with db_sessionmaker() as db:
return await user_crud.create(db, email=email, password=password, **kwargs)
return _make
+63
View File
@@ -0,0 +1,63 @@
async def test_register_login_me_refresh_logout(client, seeded):
# Register
resp = await client.post(
"/api/auth/register",
json={"email": "alice@example.com", "password": "secret123", "full_name": "Alice"},
)
assert resp.status_code == 201, resp.text
assert resp.json()["email"] == "alice@example.com"
# Login (OAuth2 form: username == email)
resp = await client.post(
"/api/auth/login",
data={"username": "alice@example.com", "password": "secret123"},
)
assert resp.status_code == 200, resp.text
tokens = resp.json()
access, refresh = tokens["access_token"], tokens["refresh_token"]
# /me with bearer token
resp = await client.get("/api/users/me", headers={"Authorization": f"Bearer {access}"})
assert resp.status_code == 200
assert resp.json()["email"] == "alice@example.com"
# Refresh rotates tokens
resp = await client.post("/api/auth/refresh", json={"refresh_token": refresh})
assert resp.status_code == 200, resp.text
new_refresh = resp.json()["refresh_token"]
assert new_refresh != refresh
# Old refresh token is now revoked
resp = await client.post("/api/auth/refresh", json={"refresh_token": refresh})
assert resp.status_code == 401
# Logout (revokes all) then the rotated refresh token is dead too
relogin = await client.post(
"/api/auth/login",
data={"username": "alice@example.com", "password": "secret123"},
)
new_access = relogin.json()["access_token"]
resp = await client.post(
"/api/auth/logout",
json={},
headers={"Authorization": f"Bearer {new_access}"},
)
assert resp.status_code == 204
resp = await client.post("/api/auth/refresh", json={"refresh_token": new_refresh})
assert resp.status_code == 401
async def test_login_wrong_password(client, seeded):
await client.post(
"/api/auth/register",
json={"email": "bob@example.com", "password": "secret123"},
)
resp = await client.post(
"/api/auth/login",
data={"username": "bob@example.com", "password": "wrong"},
)
assert resp.status_code == 401
async def test_me_requires_auth(client):
assert (await client.get("/api/users/me")).status_code == 401
+53
View File
@@ -0,0 +1,53 @@
async def _token(client, email, password="secret123"):
resp = await client.post(
"/api/auth/login", data={"username": email, "password": password}
)
assert resp.status_code == 200, resp.text
return resp.json()["access_token"]
async def test_reader_can_list_but_not_create(client, seeded, make_user):
# User with only the "user" role (grants users:read).
await make_user("reader@example.com", role_ids=[seeded["roles"]["user"].id])
token = await _token(client, "reader@example.com")
headers = {"Authorization": f"Bearer {token}"}
assert (await client.get("/api/users", headers=headers)).status_code == 200
resp = await client.post(
"/api/users",
json={"email": "new@example.com", "password": "secret123"},
headers=headers,
)
assert resp.status_code == 403
async def test_superuser_bypasses_permissions(client, seeded, make_user):
await make_user("root@example.com", is_superuser=True)
headers = {"Authorization": f"Bearer {await _token(client, 'root@example.com')}"}
resp = await client.post(
"/api/users",
json={"email": "created@example.com", "password": "secret123"},
headers=headers,
)
assert resp.status_code == 201, resp.text
# Superuser can also manage roles.
assert (await client.get("/api/roles", headers=headers)).status_code == 200
async def test_no_role_user_is_forbidden(client, seeded, make_user):
await make_user("plain@example.com") # no roles, not superuser
headers = {"Authorization": f"Bearer {await _token(client, 'plain@example.com')}"}
assert (await client.get("/api/users", headers=headers)).status_code == 403
async def test_registered_user_gets_default_role(client, seeded):
await client.post(
"/api/auth/register",
json={"email": "fresh@example.com", "password": "secret123"},
)
headers = {"Authorization": f"Bearer {await _token(client, 'fresh@example.com')}"}
me = (await client.get("/api/users/me", headers=headers)).json()
assert [r["name"] for r in me["roles"]] == ["user"]