| """ |
| Episode Feedback Security Tests |
| |
| Tests cover: |
| - Authentication requirement for episode feedback endpoint |
| - Input validation bounds for feedback_score |
| - Protection against importance score manipulation |
| """ |
| import pytest |
| from fastapi.testclient import TestClient |
| from sqlalchemy.orm import Session |
|
|
| from tests.factories.user_factory import UserFactory |
| from tests.security.conftest import create_test_token |
|
|
|
|
| class TestEpisodeFeedbackAuthentication: |
| """Test authentication requirements for episode feedback.""" |
|
|
| def test_feedback_endpoint_requires_authentication(self, client: TestClient): |
| """Test that unauthenticated requests are rejected.""" |
| response = client.post( |
| "/api/episodes/test-episode-id/feedback", |
| json={"feedback_score": 0.5} |
| ) |
|
|
| |
| assert response.status_code in [401, 403] |
|
|
| def test_feedback_endpoint_rejects_invalid_token(self, client: TestClient): |
| """Test that invalid tokens are rejected.""" |
| response = client.post( |
| "/api/episodes/test-episode-id/feedback", |
| json={"feedback_score": 0.5}, |
| headers={"Authorization": "Bearer invalid_token"} |
| ) |
|
|
| |
| assert response.status_code in [401, 403] |
|
|
|
|
| class TestEpisodeFeedbackInputValidation: |
| """Test input validation for feedback scores.""" |
|
|
| def test_feedback_score_must_be_within_bounds(self, client: TestClient, db_session: Session): |
| """Test that feedback_score must be between -1.0 and 1.0.""" |
| user = UserFactory( |
| email="test@example.com", |
| role="member", |
| _session=db_session |
| ) |
|
|
| |
| response = client.post( |
| "/api/episodes/test-episode-id/feedback", |
| json={"feedback_score": 1.5}, |
| headers={"Authorization": f"Bearer {create_test_token(user.id)}"} |
| ) |
|
|
| |
| assert response.status_code == 422 |
|
|
| def test_feedback_score_rejects_extreme_values(self, client: TestClient, db_session: Session): |
| """Test that extreme values (like the PoC 99999999.0) are rejected.""" |
| user = UserFactory( |
| email="test2@example.com", |
| role="member", |
| _session=db_session |
| ) |
|
|
| |
| response = client.post( |
| "/api/episodes/test-episode-id/feedback", |
| json={"feedback_score": 99999999.0}, |
| headers={"Authorization": f"Bearer {create_test_token(user.id)}"} |
| ) |
|
|
| |
| assert response.status_code == 422 |
|
|
| def test_feedback_score_rejects_negative_extreme(self, client: TestClient, db_session: Session): |
| """Test that extreme negative values are rejected.""" |
| user = UserFactory( |
| email="test3@example.com", |
| role="member", |
| _session=db_session |
| ) |
|
|
| response = client.post( |
| "/api/episodes/test-episode-id/feedback", |
| json={"feedback_score": -99999999.0}, |
| headers={"Authorization": f"Bearer {create_test_token(user.id)}"} |
| ) |
|
|
| |
| assert response.status_code == 422 |
|
|
| def test_feedback_score_accepts_valid_values(self, client: TestClient, db_session: Session): |
| """Test that valid feedback scores are accepted.""" |
| from core.models import Episode |
| import uuid |
|
|
| user = UserFactory( |
| email="test4@example.com", |
| role="member", |
| _session=db_session |
| ) |
|
|
| |
| episode = Episode( |
| id=str(uuid.uuid4()), |
| agent_id="test-agent", |
| session_id="test-session", |
| title="Test Episode", |
| importance_score=0.5, |
| status="completed" |
| ) |
| db_session.add(episode) |
| db_session.commit() |
|
|
| |
| for score in [-1.0, -0.5, 0.0, 0.5, 1.0]: |
| response = client.post( |
| f"/api/episodes/{episode.id}/feedback", |
| json={"feedback_score": score}, |
| headers={"Authorization": f"Bearer {create_test_token(user.id)}"} |
| ) |
|
|
| |
| |
| assert response.status_code in [200, 404] |
| assert response.status_code != 422, f"Valid score {score} was rejected with 422" |
|
|
| def test_feedback_score_rejects_wrong_type(self, client: TestClient, db_session: Session): |
| """Test that non-numeric feedback_score is rejected.""" |
| user = UserFactory( |
| email="test5@example.com", |
| role="member", |
| _session=db_session |
| ) |
|
|
| response = client.post( |
| "/api/episodes/test-episode-id/feedback", |
| json={"feedback_score": "not_a_number"}, |
| headers={"Authorization": f"Bearer {create_test_token(user.id)}"} |
| ) |
|
|
| |
| assert response.status_code == 422 |
|
|
|
|
| class TestImportanceScoreManipulation: |
| """Test protection against importance score manipulation attacks.""" |
|
|
| def test_cannot_max_out_importance_with_extreme_feedback(self, client: TestClient, db_session: Session): |
| """ |
| Test that the vulnerability PoC no longer works. |
| |
| Before fix: Sending feedback_score=99999999.0 would force importance to 1.0 |
| After fix: Should be rejected at input validation layer |
| """ |
| from core.models import Episode |
| import uuid |
|
|
| user = UserFactory( |
| email="attacker@example.com", |
| role="member", |
| _session=db_session |
| ) |
|
|
| |
| episode = Episode( |
| id=str(uuid.uuid4()), |
| agent_id="test-agent", |
| session_id="test-session", |
| title="Target Episode", |
| importance_score=0.1, |
| status="completed" |
| ) |
| db_session.add(episode) |
| db_session.commit() |
|
|
| initial_score = episode.importance_score |
|
|
| |
| response = client.post( |
| f"/api/episodes/{episode.id}/feedback", |
| json={"feedback_score": 99999999.0}, |
| headers={"Authorization": f"Bearer {create_test_token(user.id)}"} |
| ) |
|
|
| |
| assert response.status_code == 422 |
|
|
| |
| db_session.refresh(episode) |
| assert episode.importance_score == initial_score |
| assert episode.importance_score < 1.0 |
|
|
| def test_importance_score_updated_within_bounds(self, client: TestClient, db_session: Session): |
| """ |
| Test that valid feedback correctly updates importance score. |
| |
| This ensures the fix doesn't break legitimate functionality. |
| """ |
| from core.models import Episode |
| import uuid |
|
|
| user = UserFactory( |
| email="user@example.com", |
| role="member", |
| _session=db_session |
| ) |
|
|
| |
| episode = Episode( |
| id=str(uuid.uuid4()), |
| agent_id="test-agent", |
| session_id="test-session", |
| title="Test Episode", |
| importance_score=0.5, |
| status="completed" |
| ) |
| db_session.add(episode) |
| db_session.commit() |
|
|
| |
| response = client.post( |
| f"/api/episodes/{episode.id}/feedback", |
| json={"feedback_score": 1.0}, |
| headers={"Authorization": f"Bearer {create_test_token(user.id)}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 404] |
|
|
| if response.status_code == 200: |
| |
| db_session.refresh(episode) |
| |
| |
| |
| assert episode.importance_score > 0.5 |
| assert episode.importance_score <= 1.0 |
|
|
|
|
| class TestFeedbackServiceValidation: |
| """Test defensive validation in EpisodeLifecycleService.""" |
|
|
| def test_update_importance_scores_rejects_invalid_feedback(self, db_session: Session): |
| """Test that update_importance_scores rejects out-of-bounds feedback.""" |
| from core.episode_lifecycle_service import EpisodeLifecycleService |
| from core.models import Episode |
| import uuid |
|
|
| |
| episode = Episode( |
| id=str(uuid.uuid4()), |
| agent_id="test-agent", |
| session_id="test-session", |
| title="Test Episode", |
| importance_score=0.5, |
| status="completed" |
| ) |
| db_session.add(episode) |
| db_session.commit() |
|
|
| service = EpisodeLifecycleService(db_session) |
|
|
| |
| with pytest.raises(ValueError, match="Invalid feedback_score"): |
| await service.update_importance_scores(episode.id, 99999999.0) |
|
|
| |
| with pytest.raises(ValueError, match="Invalid feedback_score"): |
| await service.update_importance_scores(episode.id, -99999999.0) |
|
|
| |
| with pytest.raises(ValueError, match="Invalid feedback_score"): |
| await service.update_importance_scores(episode.id, 1.1) |
|
|
| with pytest.raises(ValueError, match="Invalid feedback_score"): |
| await service.update_importance_scores(episode.id, -1.1) |
|
|
| def test_update_importance_scores_accepts_valid_bounds(self, db_session: Session): |
| """Test that valid boundary values are accepted.""" |
| from core.episode_lifecycle_service import EpisodeLifecycleService |
| from core.models import Episode |
| import uuid |
|
|
| |
| episode = Episode( |
| id=str(uuid.uuid4()), |
| agent_id="test-agent", |
| session_id="test-session", |
| title="Test Episode", |
| importance_score=0.5, |
| status="completed" |
| ) |
| db_session.add(episode) |
| db_session.commit() |
|
|
| service = EpisodeLifecycleService(db_session) |
|
|
| |
| for score in [-1.0, 0.0, 1.0]: |
| result = await service.update_importance_scores(episode.id, score) |
| assert result is True |
|
|
| |
| db_session.refresh(episode) |
| assert 0.0 <= episode.importance_score <= 1.0 |
|
|