Spaces:
Running
Running
| """ | |
| Tests for the video search endpoint and YouTube service. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| from unittest.mock import MagicMock, patch | |
| import pytest | |
| from fastapi.testclient import TestClient | |
| # Add backend directory to path | |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) | |
| # Mock Firebase auth BEFORE importing the app | |
| from main import app as _app_import | |
| import main as main_module | |
| # Use teacher role by default for consistent behavior | |
| main_module.firebase_auth = MagicMock() | |
| main_module.firebase_auth.verify_id_token = MagicMock( | |
| return_value={ | |
| "uid": "test-teacher-uid", | |
| "email": "teacher@example.com", | |
| "role": "teacher", | |
| } | |
| ) | |
| client = TestClient(_app_import, headers={"Authorization": "Bearer test-auth-token"}) | |
| # โโโ Fixtures โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def mock_youtube_api_key(monkeypatch): | |
| monkeypatch.setenv("YOUTUBE_API_KEY", "test_youtube_api_key") | |
| def no_youtube_api_key(monkeypatch): | |
| monkeypatch.setenv("YOUTUBE_API_KEY", "") | |
| # โโโ YouTube Service Tests โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def test_parse_iso8601_duration(): | |
| from services.youtube_service import _parse_iso8601_duration | |
| assert _parse_iso8601_duration("PT5M30S") == 330 | |
| assert _parse_iso8601_duration("PT1H2M3S") == 3723 | |
| assert _parse_iso8601_duration("PT0S") == 0 | |
| assert _parse_iso8601_duration("") == 0 | |
| def test_is_educational_channel(): | |
| from services.youtube_service import _is_educational_channel | |
| assert _is_educational_channel("Khan Academy") is True | |
| assert _is_educational_channel("Math Antics") is True | |
| assert _is_educational_channel("3Blue1Brown") is True | |
| assert _is_educational_channel("Gaming Channel") is False | |
| assert _is_educational_channel("Random Vlogs") is False | |
| def test_enrich_query_with_rag_fallback(monkeypatch): | |
| """When RAG is unavailable, enrichment falls back to topic + subject.""" | |
| from services.youtube_service import _enrich_query_with_rag | |
| # Mock RAG to simulate unavailability โ patch where it's used, not where it's imported | |
| with patch("rag.curriculum_rag.retrieve_curriculum_context", side_effect=Exception("RAG unavailable")): | |
| result = _enrich_query_with_rag("quadratic equations", "General Mathematics") | |
| assert "quadratic equations" in result | |
| assert "General Mathematics" in result | |
| assert "DepEd Philippines mathematics" in result | |
| def test_get_cache_key(): | |
| from services.youtube_service import _get_cache_key | |
| key1 = _get_cache_key("quadratic equations", "General Mathematics", "Grade 11") | |
| key2 = _get_cache_key("quadratic equations", "General Mathematics", "Grade 11") | |
| key3 = _get_cache_key("linear equations", "General Mathematics", "Grade 11") | |
| assert key1 == key2 | |
| assert key1 != key3 | |
| assert len(key1) == 32 | |
| def test_cache_and_retrieve(mock_youtube_api_key, monkeypatch): | |
| from services.youtube_service import cache_videos, get_cached_videos | |
| lesson_id = "test-lesson-123" | |
| videos = [ | |
| {"videoId": "abc123", "title": "Test Video", "channelTitle": "Test Channel", | |
| "thumbnailUrl": "http://example.com/thumb.jpg", "durationSeconds": 300} | |
| ] | |
| # Mock the module-level firebase_admin and firestore in youtube_service | |
| mock_doc = MagicMock() | |
| mock_doc.get.return_value.exists = False | |
| mock_db = MagicMock() | |
| mock_db.collection.return_value.document.return_value = mock_doc | |
| mock_firebase_admin = MagicMock() | |
| mock_firebase_admin._apps = {"default": MagicMock()} | |
| mock_firestore = MagicMock() | |
| mock_firestore.client.return_value = mock_db | |
| with patch("services.youtube_service.firebase_admin", mock_firebase_admin): | |
| with patch("services.youtube_service.firestore", mock_firestore): | |
| # Store should call set | |
| cache_videos(lesson_id, videos, "quadratic equations") | |
| mock_doc.set.assert_called_once() | |
| # Retrieve should return None since we mock doc.exists = False | |
| result = get_cached_videos(lesson_id) | |
| assert result is None | |
| def test_search_youtube_videos_no_api_key(no_youtube_api_key): | |
| from services.youtube_service import search_youtube_videos | |
| result = search_youtube_videos("quadratic equations") | |
| assert result == [] | |
| # โโโ Route Tests โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def test_video_search_endpoint_no_api_key(): | |
| """Should return 503 when YouTube API key is not configured.""" | |
| with patch("routes.video_routes._get_youtube_service", return_value=(MagicMock(), "")): | |
| response = client.post("/api/lessons/videos/search", json={ | |
| "topic": "quadratic equations", | |
| "subject": "General Mathematics", | |
| "grade_level": "Grade 11", | |
| }) | |
| assert response.status_code == 503 | |
| data = response.json() | |
| assert data["detail"]["error"] == "youtube_api_not_configured" | |
| def test_video_search_endpoint_success(): | |
| """Should return video results when search succeeds.""" | |
| mock_videos = [ | |
| {"videoId": "vid1", "title": "Video 1", "channelTitle": "Channel 1", | |
| "thumbnailUrl": "http://example.com/1.jpg", "durationSeconds": 300}, | |
| {"videoId": "vid2", "title": "Video 2", "channelTitle": "Channel 2", | |
| "thumbnailUrl": "http://example.com/2.jpg", "durationSeconds": 450}, | |
| ] | |
| mock_search = MagicMock(return_value={"videos": mock_videos, "cached": False}) | |
| with patch("routes.video_routes._get_youtube_service", return_value=(mock_search, "test_key")): | |
| response = client.post("/api/lessons/videos/search", json={ | |
| "topic": "quadratic equations", | |
| "subject": "General Mathematics", | |
| "grade_level": "Grade 11", | |
| "lesson_id": "lesson-123", | |
| }) | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert len(data["videos"]) == 2 | |
| assert data["cached"] is False | |
| assert data["videos"][0]["videoId"] == "vid1" | |
| def test_video_search_endpoint_empty_results(): | |
| """Should return empty list when no videos found.""" | |
| mock_search = MagicMock(return_value={"videos": [], "cached": False}) | |
| with patch("routes.video_routes._get_youtube_service", return_value=(mock_search, "test_key")): | |
| response = client.post("/api/lessons/videos/search", json={ | |
| "topic": "very obscure topic xyz123", | |
| "subject": "General Mathematics", | |
| }) | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["videos"] == [] | |
| assert data["cached"] is False | |
| def test_video_search_endpoint_cached(): | |
| """Should return cached results.""" | |
| mock_videos = [ | |
| {"videoId": "vid1", "title": "Cached Video", "channelTitle": "Channel 1", | |
| "thumbnailUrl": "http://example.com/1.jpg", "durationSeconds": 300}, | |
| ] | |
| mock_search = MagicMock(return_value={"videos": mock_videos, "cached": True}) | |
| with patch("routes.video_routes._get_youtube_service", return_value=(mock_search, "test_key")): | |
| response = client.post("/api/lessons/videos/search", json={ | |
| "topic": "linear equations", | |
| "lesson_id": "lesson-456", | |
| }) | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["cached"] is True | |
| assert len(data["videos"]) == 1 | |
| def test_video_search_endpoint_validation_error(): | |
| """Should return 422 when topic is missing or too long.""" | |
| response = client.post("/api/lessons/videos/search", json={ | |
| "topic": "", | |
| "subject": "General Mathematics", | |
| }) | |
| assert response.status_code == 422 | |
| response = client.post("/api/lessons/videos/search", json={ | |
| "topic": "x" * 201, | |
| "subject": "General Mathematics", | |
| }) | |
| assert response.status_code == 422 | |