""" Tests for enhanced API authentication with IP whitelisting and better error handling. """ import pytest pytestmark = [pytest.mark.unit, pytest.mark.utils] from datetime import datetime, timedelta from unittest.mock import Mock, patch, MagicMock from flask import Flask, g from app.models import ApiToken, User from app.utils.api_auth import authenticate_token, require_api_token, extract_token_from_request from app import db class TestExtractToken: """Tests for token extraction from requests""" def test_extract_from_bearer_header(self): """Test extracting token from Bearer Authorization header""" from flask import Flask, request app = Flask(__name__) with app.test_request_context(headers={"Authorization": "Bearer tt_testtoken123"}): token = extract_token_from_request() assert token == "tt_testtoken123" def test_extract_from_token_header(self): """Test extracting token from Token Authorization header""" from flask import Flask, request app = Flask(__name__) with app.test_request_context(headers={"Authorization": "Token tt_testtoken123"}): token = extract_token_from_request() assert token == "tt_testtoken123" def test_extract_from_api_key_header(self): """Test extracting token from X-API-Key header""" from flask import Flask, request app = Flask(__name__) with app.test_request_context(headers={"X-API-Key": "tt_testtoken123"}): token = extract_token_from_request() assert token == "tt_testtoken123" def test_extract_none_when_missing(self): """Test that None is returned when no token is present""" from flask import Flask, request app = Flask(__name__) with app.test_request_context(): token = extract_token_from_request() assert token is None class TestAuthenticateToken: """Tests for token authentication with enhanced security""" @pytest.fixture def sample_user(self, app): """Create a sample user for testing""" user = User(username="testuser", is_active=True) db.session.add(user) db.session.commit() return user @pytest.fixture def sample_token(self, app, sample_user): """Create a sample API token""" token, plain_token = ApiToken.create_token( user_id=sample_user.id, name="Test Token", scopes="read:projects", expires_days=30 ) db.session.add(token) db.session.commit() return token, plain_token def test_authenticate_valid_token(self, app, sample_user, sample_token): """Test authentication with valid token""" token, plain_token = sample_token with app.test_request_context(remote_addr="127.0.0.1"): user, api_token, error = authenticate_token(plain_token) assert user is not None assert api_token is not None assert error is None assert user.id == sample_user.id assert api_token.id == token.id def test_authenticate_expired_token(self, app, sample_user): """Test authentication with expired token""" token, plain_token = ApiToken.create_token( user_id=sample_user.id, name="Expired Token", expires_days=-1 # Expired ) token.expires_at = datetime.utcnow() - timedelta(days=1) db.session.add(token) db.session.commit() with app.test_request_context(remote_addr="127.0.0.1"): user, api_token, error = authenticate_token(plain_token) assert user is None assert api_token is None assert error == "Token has expired" def test_authenticate_revoked_token(self, app, sample_user, sample_token): """Test authentication with revoked token""" token, plain_token = sample_token token.is_active = False db.session.commit() with app.test_request_context(remote_addr="127.0.0.1"): user, api_token, error = authenticate_token(plain_token) assert user is None assert api_token is None assert error == "Token has been revoked" def test_authenticate_with_ip_whitelist_allowed(self, app, sample_user): """Test authentication with IP whitelist - allowed IP""" token, plain_token = ApiToken.create_token( user_id=sample_user.id, name="Whitelisted Token", scopes="read:projects" ) token.ip_whitelist = "127.0.0.1,192.168.1.0/24" db.session.add(token) db.session.commit() with app.test_request_context(remote_addr="127.0.0.1"): user, api_token, error = authenticate_token(plain_token) assert user is not None assert api_token is not None assert error is None def test_authenticate_with_ip_whitelist_denied(self, app, sample_user): """Test authentication with IP whitelist - denied IP""" token, plain_token = ApiToken.create_token( user_id=sample_user.id, name="Whitelisted Token", scopes="read:projects" ) token.ip_whitelist = "192.168.1.0/24" db.session.add(token) db.session.commit() with app.test_request_context(remote_addr="10.0.0.1"): user, api_token, error = authenticate_token(plain_token) assert user is None assert api_token is None assert error == "Access denied from this IP address" def test_authenticate_with_cidr_block(self, app, sample_user): """Test authentication with CIDR block in whitelist""" token, plain_token = ApiToken.create_token(user_id=sample_user.id, name="CIDR Token", scopes="read:projects") token.ip_whitelist = "192.168.1.0/24" db.session.add(token) db.session.commit() with app.test_request_context(remote_addr="192.168.1.100"): user, api_token, error = authenticate_token(plain_token) assert user is not None assert api_token is not None assert error is None def test_authenticate_invalid_token_format(self, app): """Test authentication with invalid token format""" with app.test_request_context(remote_addr="127.0.0.1"): user, api_token, error = authenticate_token("invalid_token") assert user is None assert api_token is None assert error == "Invalid token format" def test_authenticate_nonexistent_token(self, app): """Test authentication with non-existent token""" fake_token = "tt_" + "x" * 32 with app.test_request_context(remote_addr="127.0.0.1"): user, api_token, error = authenticate_token(fake_token) assert user is None assert api_token is None assert error == "Token not found" def test_authenticate_inactive_user(self, app, sample_token): """Test authentication with inactive user""" token, plain_token = sample_token token.user.is_active = False db.session.commit() with app.test_request_context(remote_addr="127.0.0.1"): user, api_token, error = authenticate_token(plain_token) assert user is None assert api_token is None assert error == "User account is inactive" class TestRequireApiToken: """Tests for require_api_token decorator""" @pytest.fixture def app_with_routes(self, app): """Create Flask app with test routes""" @app.route("/test/protected") @require_api_token("read:projects") def protected_route(): return {"message": "success", "user_id": g.api_user.id} @app.route("/test/protected_no_scope") @require_api_token() def protected_route_no_scope(): return {"message": "success"} return app def test_protected_route_with_valid_token(self, app_with_routes, sample_user, sample_token): """Test accessing protected route with valid token""" token, plain_token = sample_token with app_with_routes.test_client() as client: response = client.get("/test/protected", headers={"Authorization": f"Bearer {plain_token}"}) assert response.status_code == 200 data = response.get_json() assert data["message"] == "success" assert data["user_id"] == sample_user.id def test_protected_route_without_token(self, app_with_routes): """Test accessing protected route without token""" with app_with_routes.test_client() as client: response = client.get("/test/protected") assert response.status_code == 401 data = response.get_json() assert "error" in data assert "Authentication required" in data["error"] def test_protected_route_with_insufficient_scope(self, app_with_routes, sample_user): """Test accessing protected route with insufficient scope""" token, plain_token = ApiToken.create_token( user_id=sample_user.id, name="Limited Token", scopes="read:time_entries" # Different scope ) db.session.add(token) db.session.commit() with app_with_routes.test_client() as client: response = client.get("/test/protected", headers={"Authorization": f"Bearer {plain_token}"}) assert response.status_code == 403 data = response.get_json() assert "error" in data assert "Insufficient permissions" in data["error"] def test_protected_route_with_wildcard_scope(self, app_with_routes, sample_user): """Test accessing protected route with wildcard scope""" token, plain_token = ApiToken.create_token( user_id=sample_user.id, name="Admin Token", scopes="read:*" # Wildcard scope ) db.session.add(token) db.session.commit() with app_with_routes.test_client() as client: response = client.get("/test/protected", headers={"Authorization": f"Bearer {plain_token}"}) assert response.status_code == 200 data = response.get_json() assert data["message"] == "success"