brain / tests /test_auth_http_contract.py
Leon4gr45's picture
Deploy complete BrainAPI source d15a5889e878
2f1e7ff verified
Raw History Blame Contribute Delete
9.26 kB
"""Real HTTP middleware/routes with controlled persistence boundaries."""
from datetime import datetime,timezone
from fastapi import FastAPI
from fastapi.testclient import TestClient
import pytest
from src.constants.data import Brain
from src.services.auth import AuthService
from src.services.credentials import PostgreSQLTokenRepository
from src.services.console_sessions import ConsoleSessions,SESSION_COOKIE
from src.services.api.errors import install_error_handlers
from src.services.api.middlewares import auth as middleware,brains
from src.services.api.routes import auth,access_tokens,meta,workspaces,workspace_api,agent,ingest,public,system
from src.services.api.controllers import system as system_controller
from tests.test_console_sessions import Redis
from tests.test_workspaces import MemoryRepository
class Store(PostgreSQLTokenRepository):
"""Substitute SQL execution only; issuance, hashing, metadata and revocation use production code."""
def __init__(self): super().__init__();self.rows={}
def _query(self,sql,args=(),many=False,write=False):
if 'INSERT INTO access_tokens' in sql:
keys=['id','label','brain_id','brain_resource_id','token_prefix','token_hash','permissions','created_by','expires_at']
row=dict(zip(keys,args));row.update(created_at=datetime.now(timezone.utc),last_used_at=None,revoked_at=None,legacy=False)
self.rows[row['id']]=row;return row.copy()
if sql.startswith('SELECT'):
rows=list(self.rows.values())
if 'token_hash=' in sql: rows=[r for r in rows if r['token_hash']==args[0]]
elif 'id=' in sql: rows=[r for r in rows if r['id']==args[0]]
elif 'brain_id=' in sql: rows=[r for r in rows if r['brain_id']==args[0]]
return [r.copy() for r in rows] if many else rows[0].copy() if rows else None
row=self.rows.get(args[0])
if row is None:return None
row['revoked_at' if 'revoked_at' in sql else 'last_used_at']=datetime.now(timezone.utc)
return row.copy()
class Registry(MemoryRepository):
def get_brain_by_pat(self,pat): return next((b for b in self.brains.values() if b.pat==pat),None)
@pytest.fixture
def http(monkeypatch):
monkeypatch.setenv('SYSTEM_PAT','canonical-admin');monkeypatch.delenv('BRAINPAT_TOKEN',raising=False)
monkeypatch.delenv('PUBLIC_BASE_URL',raising=False)
registry=Registry([Brain(name_key='alpha'),Brain(name_key='beta')]);store=Store();sessions=ConsoleSessions(Redis())
from src.services.workspaces import WorkspaceService
WorkspaceService(registry).bootstrap()
service=AuthService(registry,store)
for module in [middleware,auth,access_tokens]: monkeypatch.setattr(module,'get_auth_service',lambda *args:service)
monkeypatch.setattr(middleware,'get_console_sessions',lambda:sessions);monkeypatch.setattr(auth,'get_console_sessions',lambda:sessions)
for module in [brains,workspaces,workspace_api,system_controller,agent]: monkeypatch.setattr(module,'data_adapter',registry)
monkeypatch.setattr(access_tokens,'token_repository',store)
app=FastAPI();install_error_handlers(app);app.add_middleware(middleware.BrainPATMiddleware);app.add_middleware(brains.BrainMiddleware)
for router in [auth.auth_router,access_tokens.access_token_router,meta.meta_router,workspaces.workspace_router,workspace_api.workspace_api_router,agent.agent_router,ingest.ingest_router,public.public_router,system.system_router]:app.include_router(router)
return TestClient(app,base_url='https://testserver'),registry,store,sessions
ADMIN={'Authorization':'Bearer canonical-admin'}
def issue(client,permissions=None):
response=client.post('/system/access-tokens',headers=ADMIN,json={'brain_id':'alpha','label':'agent-prod','permissions':permissions or ['memory:read','memory:write']})
assert response.status_code==201,response.text
return response.json()
def test_system_cookie_login_csrf_and_logout(http):
client,registry,store,sessions=http
response=client.post('/auth/session',json={'token':'canonical-admin'})
assert response.status_code==200
cookie=response.headers['set-cookie'];assert 'HttpOnly' in cookie and 'Secure' in cookie and 'SameSite=strict' in cookie
assert 'canonical-admin' not in response.text
assert client.get('/auth/me').json()['brain_id'] is None
assert client.get('/meta/login-info').json()=={'is_system_pat':True,'brain_id':None}
assert client.get('/system/workspaces').status_code==200
assert client.post('/system/brains',json={'brain_id':'third'}).status_code==403
csrf=response.json()['csrf_token']
assert client.post('/system/brains',json={'brain_id':'third'},headers={'X-CSRF-Token':csrf}).status_code==200
assert client.post('/auth/logout',headers={'X-CSRF-Token':csrf}).status_code==200
assert client.get('/auth/me').status_code==401
def test_token_lifecycle_and_session_revocation(http):
client,registry,store,sessions=http
issued=issue(client);token=issued['token'];headers={'Authorization':'Bearer '+token}
assert token not in repr(store.rows)
assert client.get('/system/access-tokens',headers=headers).status_code==403
listing=client.get('/system/access-tokens',headers=ADMIN).json()['tokens']
assert 'token' not in listing[0] and 'token_hash' not in listing[0]
assert client.get('/brains/alpha/agent/capabilities',headers=headers).status_code==200
assert client.get('/brains/beta/agent/capabilities',headers=headers).status_code==403
login=client.post('/auth/session',json={'token':token});assert login.status_code==200
assert client.get('/auth/me').json()['brain_id']=='alpha'
assert {w['brain_id'] for w in client.get('/system/workspaces').json()['workspaces']}=={'alpha'}
assert client.delete('/system/access-tokens/'+issued['id'],headers=ADMIN).status_code==200
assert client.get('/brains/alpha/agent/capabilities',headers=headers).status_code==401
assert client.get('/auth/me').status_code==401
@pytest.mark.parametrize('path',['/auth/me','/meta/login-info','/system/workspaces'])
def test_invalid_and_missing_token_401(http,path):
client,*_=http
assert client.get(path).status_code==401
assert client.get(path,headers={'Authorization':'Bearer unknown'}).status_code==401
def test_credential_free_creation_and_listings(http):
client,registry,store,sessions=http
assert 'pat' not in client.post('/system/brains',headers=ADMIN,json={'brain_id':'new-brain'}).json()
assert 'pat' not in client.post('/system/workspaces',headers=ADMIN,json={'slug':'new-workspace','display_name':'New workspace'}).json()
assert all(brain.pat is None for brain in registry.brains.values())
assert 'pat' not in client.get('/system/brains-list',headers=ADMIN).text
assert 'pat' not in client.get('/system/workspaces',headers=ADMIN).text
@pytest.mark.parametrize('path',['/ingest/structured','/brains/alpha/api/ingest/structured'])
def test_exception_containing_structured_validation_is_422(http,path):
client,*_=http
response=client.post(path,headers={**ADMIN,'X-Brain-ID':'alpha'},json={'data':[{'object':{'name':'only-object','type':'ENTITY'}}]})
assert response.status_code==422,response.text
body=response.json();assert body['error']['code']=='VALIDATION_ERROR'
assert isinstance(body['detail'][0]['ctx']['error'],str)
assert 'input' not in body['detail'][0]
def test_read_only_token_cannot_ingest(http):
client,*_=http;issued=issue(client,['memory:read'])
response=client.post('/brains/alpha/api/ingest/',headers={'Authorization':'Bearer '+issued['token']},json={'data':{'data_type':'text','text_data':'example'}})
assert response.status_code==403
def test_invalid_login_and_cross_origin_creates_no_session(http):
client,registry,store,sessions=http
response=client.post('/auth/session',json={'token':'unknown'})
assert response.status_code==401 and 'set-cookie' not in response.headers
response=client.post('/auth/session',json={'token':'canonical-admin'},headers={'Origin':'https://evil.example'})
assert response.status_code==403 and 'set-cookie' not in response.headers
assert not sessions.redis.values
def test_auth_openapi_responses_are_typed_and_token_listing_has_no_secret_fields(http):
client,*_=http
schema=client.get('/openapi.json').json()
for path,method in [('/auth/session','post'),('/auth/me','get'),('/auth/logout','post'),('/system/access-tokens','get'),('/system/access-tokens','post')]:
status='201' if path=='/system/access-tokens' and method=='post' else '200'
assert schema['paths'][path][method]['responses'][status]['content']['application/json']['schema'].get('$ref')
metadata=schema['components']['schemas']['AccessTokenMetadataResponse']['properties']
assert 'token' not in metadata and 'token_hash' not in metadata
def test_cookie_csrf_origin_failure_is_forbidden_not_service_unavailable(http):
client,*_=http
login=client.post('/auth/session',json={'token':'canonical-admin'})
response=client.post('/auth/logout',headers={'X-CSRF-Token':login.json()['csrf_token'],'Origin':'https://evil.example'})
assert response.status_code==403
assert client.get('/auth/me').status_code==200