#!/usr/bin/env python3 """ Tests for the enforce_auth before_request hook in api/app.py. The hook has two distinct behaviours depending on the auth store state: - users file exists and is POPULATED → auth is enforced (unauthenticated → 401) - users file exists but is EMPTY → 503 (auth not configured) - users file does not exist / unreadable → bypass (pre-auth compat mode) These tests create real AuthManager instances pointing at tmp directories so that list_users() and the file-readability check both behave exactly as they do in production. """ import os import sys import json import pytest from pathlib import Path from unittest.mock import patch sys.path.insert(0, str(Path(__file__).parent.parent / 'api')) @pytest.fixture def flask_client(): from app import app app.config['TESTING'] = True return app.test_client() @pytest.fixture def populated_auth_manager(tmp_path): """AuthManager whose users file contains at least one admin account.""" from auth_manager import AuthManager data_dir = str(tmp_path / 'data') config_dir = str(tmp_path / 'config') os.makedirs(data_dir, exist_ok=True) os.makedirs(config_dir, exist_ok=True) mgr = AuthManager(data_dir=data_dir, config_dir=config_dir) # Create an admin so list_users() is non-empty ok = mgr.create_user('admin', 'AdminPass123!', 'admin') assert ok, 'Could not seed admin user for test' return mgr @pytest.fixture def empty_auth_manager(tmp_path): """AuthManager whose users file exists and is readable but contains no users.""" from auth_manager import AuthManager data_dir = str(tmp_path / 'data') config_dir = str(tmp_path / 'config') os.makedirs(data_dir, exist_ok=True) os.makedirs(config_dir, exist_ok=True) mgr = AuthManager(data_dir=data_dir, config_dir=config_dir) # Explicitly create the file with an empty list to simulate the # "auth configured but no users" misconfiguration scenario. users_file = os.path.join(data_dir, 'auth_users.json') with open(users_file, 'w') as f: f.write('[]') assert mgr.list_users() == [], 'Expected empty user list' return mgr # ── populated store → auth enforced ────────────────────────────────────────── def test_populated_auth_manager_unauthenticated_request_gets_401( flask_client, populated_auth_manager ): """When the auth store has users, unauthenticated API requests must get 401.""" with patch('app.auth_manager', populated_auth_manager): r = flask_client.get('/api/status') assert r.status_code == 401 data = json.loads(r.data) assert 'error' in data def test_populated_auth_manager_401_body_says_not_authenticated( flask_client, populated_auth_manager ): """The 401 body must clearly indicate the session is missing.""" with patch('app.auth_manager', populated_auth_manager): r = flask_client.get('/api/peers') assert r.status_code == 401 data = json.loads(r.data) assert 'Not authenticated' in data.get('error', '') def test_populated_auth_manager_non_api_path_bypasses_auth( flask_client, populated_auth_manager ): """Non-API paths like /health must always be public.""" with patch('app.auth_manager', populated_auth_manager): r = flask_client.get('/health') assert r.status_code == 200 def test_populated_auth_manager_auth_namespace_bypasses_auth( flask_client, populated_auth_manager ): """The /api/auth/* namespace must always be accessible without a session.""" with patch('app.auth_manager', populated_auth_manager): r = flask_client.get('/api/auth/me') # /api/auth/me may return 401 from the route itself (no session), but it # must NOT be blocked by enforce_auth; the enforce_auth hook must return None # for /api/auth/* paths. The status must not be 503. assert r.status_code != 503 # ── empty store → 503 ──────────────────────────────────────────────────────── def test_empty_auth_manager_returns_503_for_api_requests( flask_client, empty_auth_manager ): """When the users file exists and is readable but empty, /api/* must get 503.""" with patch('app.auth_manager', empty_auth_manager): r = flask_client.get('/api/status') assert r.status_code == 503 data = json.loads(r.data) assert 'error' in data def test_empty_auth_manager_503_body_mentions_configuration( flask_client, empty_auth_manager ): """The 503 error body must mention that auth is not configured.""" with patch('app.auth_manager', empty_auth_manager): r = flask_client.get('/api/config') assert r.status_code == 503 data = json.loads(r.data) error_text = data.get('error', '') assert 'not configured' in error_text.lower() or 'Authentication' in error_text def test_empty_auth_manager_non_api_path_bypasses_503( flask_client, empty_auth_manager ): """Even with an empty auth store, /health must remain accessible.""" with patch('app.auth_manager', empty_auth_manager): r = flask_client.get('/health') assert r.status_code == 200 # ── role-based access: peer vs admin ───────────────────────────────────────── @pytest.fixture def peer_client(tmp_path): """Test client with a peer-role session active.""" from app import app from auth_manager import AuthManager data_dir = str(tmp_path / 'data') config_dir = str(tmp_path / 'config') os.makedirs(data_dir, exist_ok=True) os.makedirs(config_dir, exist_ok=True) mgr = AuthManager(data_dir=data_dir, config_dir=config_dir) mgr.create_user('admin', 'AdminPass123!', 'admin') mgr.create_user('alice', 'AlicePass123!', 'peer') app.config['TESTING'] = True with patch('app.auth_manager', mgr): try: import auth_routes with patch.object(auth_routes, 'auth_manager', mgr, create=True): with app.test_client() as client: r = client.post('/api/auth/login', data=json.dumps({'username': 'alice', 'password': 'AlicePass123!'}), content_type='application/json') assert r.status_code == 200, f'peer login failed: {r.status_code}' yield client, mgr except ImportError: with app.test_client() as client: with client.session_transaction() as sess: sess['username'] = 'alice' sess['role'] = 'peer' yield client, mgr def test_peer_role_blocked_from_admin_only_endpoint(peer_client): """Peer sessions must not access admin-only endpoints like /api/peers.""" client, mgr = peer_client with patch('app.auth_manager', mgr): r = client.get('/api/peers') assert r.status_code == 403 def test_peer_role_allowed_services_active(peer_client): """/api/services/active must be accessible to peer sessions. Regression guard: peers saw 'not installed' on My Services because enforce_auth returned 403 for this endpoint. """ client, mgr = peer_client with patch('app.auth_manager', mgr): r = client.get('/api/services/active') # 200 (or whatever the route returns) but NOT 403 assert r.status_code != 403, ( '/api/services/active returned 403 for peer — peer UI cannot show installed services' ) def test_admin_role_still_allowed_services_active(flask_client, populated_auth_manager): """/api/services/active must remain accessible to admin sessions.""" with patch('app.auth_manager', populated_auth_manager): try: import auth_routes with patch.object(auth_routes, 'auth_manager', populated_auth_manager, create=True): r_login = flask_client.post('/api/auth/login', data=json.dumps({'username': 'admin', 'password': 'AdminPass123!'}), content_type='application/json') assert r_login.status_code == 200 r = flask_client.get('/api/services/active') except ImportError: with flask_client.session_transaction() as sess: sess['username'] = 'admin' sess['role'] = 'admin' r = flask_client.get('/api/services/active') assert r.status_code != 403 if __name__ == '__main__': pytest.main([__file__, '-v'])