#!/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) # The constructor creates the file with '[]' (empty list). We do NOT add # any user, so list_users() returns [] but the file is readable. 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 if __name__ == '__main__': pytest.main([__file__, '-v'])