- Implemented comprehensive test suite for page controller - 37 unit tests covering: - Root endpoint (/) rendering index.html - Setup endpoint (/setup) rendering setup.html - Login endpoint (/login) rendering login.html - Queue endpoint (/queue) rendering queue.html - Loading endpoint (/loading) rendering loading.html - Template helper functions for context generation - Series context preparation and filtering - Template validation and listing - Series lookup by key - Filter series by missing episodes Coverage: - Page controller: 100% (19/19 statements) - Template helpers: 98.28% (42/42 statements, 15/16 branches) - Overall: Exceeds 85%+ target Test results: All 37 tests passing - Mocked render_template for controller tests - Mocked Request objects - Tested all template helper functions - Validated correct template names and titles passed
579 lines
22 KiB
Python
579 lines
22 KiB
Python
"""Unit tests for page controller.
|
|
|
|
This module tests the FastAPI page controller that serves HTML templates
|
|
for various application pages.
|
|
|
|
Coverage Target: 85%+
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from fastapi import Request
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_request():
|
|
"""Create a mock FastAPI request."""
|
|
request = MagicMock(spec=Request)
|
|
request.url = MagicMock()
|
|
request.url.path = "/"
|
|
request.headers = {}
|
|
return request
|
|
|
|
|
|
class TestRootEndpoint:
|
|
"""Test the root / endpoint."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_root_endpoint_calls_render_template(self, mock_request):
|
|
"""Test root endpoint calls render_template."""
|
|
from src.server.controllers.page_controller import root
|
|
|
|
with patch('src.server.controllers.page_controller.render_template') as mock_render:
|
|
mock_render.return_value = "<!DOCTYPE html><html></html>"
|
|
|
|
result = await root(mock_request)
|
|
|
|
mock_render.assert_called_once()
|
|
call_args = mock_render.call_args[0]
|
|
assert call_args[0] == "index.html"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_root_endpoint_passes_correct_title(self, mock_request):
|
|
"""Test root endpoint passes correct title."""
|
|
from src.server.controllers.page_controller import root
|
|
|
|
with patch('src.server.controllers.page_controller.render_template') as mock_render:
|
|
mock_render.return_value = "<!DOCTYPE html><html></html>"
|
|
|
|
await root(mock_request)
|
|
|
|
call_kwargs = mock_render.call_args[1]
|
|
assert call_kwargs["title"] == "Aniworld Download Manager"
|
|
|
|
|
|
class TestSetupEndpoint:
|
|
"""Test the /setup endpoint."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_setup_endpoint_renders_setup_html(self, mock_request):
|
|
"""Test setup endpoint renders setup.html template."""
|
|
from src.server.controllers.page_controller import setup_page
|
|
|
|
with patch('src.server.controllers.page_controller.render_template') as mock_render:
|
|
mock_render.return_value = "<!DOCTYPE html><html></html>"
|
|
|
|
await setup_page(mock_request)
|
|
|
|
call_args = mock_render.call_args[0]
|
|
assert call_args[0] == "setup.html"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_setup_endpoint_title(self, mock_request):
|
|
"""Test setup endpoint passes correct title."""
|
|
from src.server.controllers.page_controller import setup_page
|
|
|
|
with patch('src.server.controllers.page_controller.render_template') as mock_render:
|
|
mock_render.return_value = "<!DOCTYPE html><html></html>"
|
|
|
|
await setup_page(mock_request)
|
|
|
|
call_kwargs = mock_render.call_args[1]
|
|
assert call_kwargs["title"] == "Setup - Aniworld"
|
|
|
|
|
|
class TestLoginEndpoint:
|
|
"""Test the /login endpoint."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_endpoint_renders_login_html(self, mock_request):
|
|
"""Test login endpoint renders login.html template."""
|
|
from src.server.controllers.page_controller import login_page
|
|
|
|
with patch('src.server.controllers.page_controller.render_template') as mock_render:
|
|
mock_render.return_value = "<!DOCTYPE html><html></html>"
|
|
|
|
await login_page(mock_request)
|
|
|
|
call_args = mock_render.call_args[0]
|
|
assert call_args[0] == "login.html"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_endpoint_title(self, mock_request):
|
|
"""Test login endpoint passes correct title."""
|
|
from src.server.controllers.page_controller import login_page
|
|
|
|
with patch('src.server.controllers.page_controller.render_template') as mock_render:
|
|
mock_render.return_value = "<!DOCTYPE html><html></html>"
|
|
|
|
await login_page(mock_request)
|
|
|
|
call_kwargs = mock_render.call_args[1]
|
|
assert call_kwargs["title"] == "Login - Aniworld"
|
|
|
|
|
|
class TestQueueEndpoint:
|
|
"""Test the /queue endpoint."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_queue_endpoint_renders_queue_html(self, mock_request):
|
|
"""Test queue endpoint renders queue.html template."""
|
|
from src.server.controllers.page_controller import queue_page
|
|
|
|
with patch('src.server.controllers.page_controller.render_template') as mock_render:
|
|
mock_render.return_value = "<!DOCTYPE html><html></html>"
|
|
|
|
await queue_page(mock_request)
|
|
|
|
call_args = mock_render.call_args[0]
|
|
assert call_args[0] == "queue.html"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_queue_endpoint_title(self, mock_request):
|
|
"""Test queue endpoint passes correct title."""
|
|
from src.server.controllers.page_controller import queue_page
|
|
|
|
with patch('src.server.controllers.page_controller.render_template') as mock_render:
|
|
mock_render.return_value = "<!DOCTYPE html><html></html>"
|
|
|
|
await queue_page(mock_request)
|
|
|
|
call_kwargs = mock_render.call_args[1]
|
|
assert call_kwargs["title"] == "Download Queue - Aniworld"
|
|
|
|
|
|
class TestLoadingEndpoint:
|
|
"""Test the /loading endpoint."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_loading_endpoint_renders_loading_html(self, mock_request):
|
|
"""Test loading endpoint renders loading.html template."""
|
|
from src.server.controllers.page_controller import loading_page
|
|
|
|
with patch('src.server.controllers.page_controller.render_template') as mock_render:
|
|
mock_render.return_value = "<!DOCTYPE html><html></html>"
|
|
|
|
await loading_page(mock_request)
|
|
|
|
call_args = mock_render.call_args[0]
|
|
assert call_args[0] == "loading.html"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_loading_endpoint_title(self, mock_request):
|
|
"""Test loading endpoint passes correct title."""
|
|
from src.server.controllers.page_controller import loading_page
|
|
|
|
with patch('src.server.controllers.page_controller.render_template') as mock_render:
|
|
mock_render.return_value = "<!DOCTYPE html><html></html>"
|
|
|
|
await loading_page(mock_request)
|
|
|
|
call_kwargs = mock_render.call_args[1]
|
|
assert call_kwargs["title"] == "Initializing - Aniworld"
|
|
|
|
|
|
class TestTemplateHelpers:
|
|
"""Test template helper functions."""
|
|
|
|
def test_get_base_context(self):
|
|
"""Test getting base context."""
|
|
from src.server.utils.template_helpers import get_base_context
|
|
|
|
mock_request = MagicMock(spec=Request)
|
|
context = get_base_context(mock_request, "Test Title")
|
|
|
|
assert context["request"] == mock_request
|
|
assert context["title"] == "Test Title"
|
|
assert context["app_name"] == "Aniworld Download Manager"
|
|
assert context["version"] == "1.0.0"
|
|
|
|
def test_get_base_context_default_title(self):
|
|
"""Test getting base context with default title."""
|
|
from src.server.utils.template_helpers import get_base_context
|
|
|
|
mock_request = MagicMock(spec=Request)
|
|
context = get_base_context(mock_request)
|
|
|
|
assert context["title"] == "Aniworld"
|
|
|
|
def test_render_template_basic(self):
|
|
"""Test render_template function."""
|
|
from src.server.utils.template_helpers import render_template
|
|
|
|
mock_request = MagicMock(spec=Request)
|
|
|
|
with patch('src.server.utils.template_helpers.templates.TemplateResponse') as mock_response:
|
|
mock_response.return_value = "rendered"
|
|
|
|
result = render_template("index.html", mock_request, title="Test")
|
|
|
|
assert result == "rendered"
|
|
mock_response.assert_called_once()
|
|
|
|
def test_render_template_with_context(self):
|
|
"""Test render_template with additional context."""
|
|
from src.server.utils.template_helpers import render_template
|
|
|
|
mock_request = MagicMock(spec=Request)
|
|
extra_context = {"user": "test_user", "is_authenticated": True}
|
|
|
|
with patch('src.server.utils.template_helpers.templates.TemplateResponse') as mock_response:
|
|
mock_response.return_value = "rendered"
|
|
|
|
render_template("index.html", mock_request, context=extra_context)
|
|
|
|
call_args = mock_response.call_args[0]
|
|
context_arg = call_args[1]
|
|
assert context_arg["user"] == "test_user"
|
|
assert context_arg["is_authenticated"] is True
|
|
|
|
def test_render_template_title_generation(self):
|
|
"""Test render_template generates title from template name."""
|
|
from src.server.utils.template_helpers import render_template
|
|
|
|
mock_request = MagicMock(spec=Request)
|
|
|
|
with patch('src.server.utils.template_helpers.templates.TemplateResponse') as mock_response:
|
|
mock_response.return_value = "rendered"
|
|
|
|
render_template("index.html", mock_request) # No title provided
|
|
|
|
call_args = mock_response.call_args[0]
|
|
context_arg = call_args[1]
|
|
# Title should be generated from template name
|
|
assert context_arg["title"] == "Index"
|
|
|
|
|
|
class TestValidateTemplateExists:
|
|
"""Test template existence validation."""
|
|
|
|
def test_validate_template_exists_true(self, tmp_path):
|
|
"""Test validate_template_exists returns True for existing template."""
|
|
from src.server.utils.template_helpers import validate_template_exists
|
|
|
|
with patch('src.server.utils.template_helpers.TEMPLATES_DIR', tmp_path):
|
|
template_file = tmp_path / "test.html"
|
|
template_file.write_text("<html></html>")
|
|
|
|
result = validate_template_exists("test.html")
|
|
assert result is True
|
|
|
|
def test_validate_template_exists_false(self, tmp_path):
|
|
"""Test validate_template_exists returns False for missing template."""
|
|
from src.server.utils.template_helpers import validate_template_exists
|
|
|
|
with patch('src.server.utils.template_helpers.TEMPLATES_DIR', tmp_path):
|
|
result = validate_template_exists("nonexistent.html")
|
|
assert result is False
|
|
|
|
|
|
class TestListAvailableTemplates:
|
|
"""Test listing available templates."""
|
|
|
|
def test_list_available_templates(self, tmp_path):
|
|
"""Test listing available templates."""
|
|
from src.server.utils.template_helpers import list_available_templates
|
|
|
|
# Create test template files
|
|
(tmp_path / "index.html").write_text("<html></html>")
|
|
(tmp_path / "setup.html").write_text("<html></html>")
|
|
(tmp_path / "README.md").write_text("# README") # Should not be included
|
|
|
|
with patch('src.server.utils.template_helpers.TEMPLATES_DIR', tmp_path):
|
|
templates = list_available_templates()
|
|
|
|
assert "index.html" in templates
|
|
assert "setup.html" in templates
|
|
assert "README.md" not in templates
|
|
assert len(templates) == 2
|
|
|
|
def test_list_available_templates_empty_directory(self, tmp_path):
|
|
"""Test listing templates from empty directory."""
|
|
from src.server.utils.template_helpers import list_available_templates
|
|
|
|
with patch('src.server.utils.template_helpers.TEMPLATES_DIR', tmp_path):
|
|
templates = list_available_templates()
|
|
|
|
assert templates == []
|
|
|
|
def test_list_available_templates_missing_directory(self):
|
|
"""Test listing templates when directory doesn't exist."""
|
|
from src.server.utils.template_helpers import list_available_templates
|
|
|
|
with patch('src.server.utils.template_helpers.TEMPLATES_DIR', Path("/nonexistent")):
|
|
templates = list_available_templates()
|
|
|
|
assert templates == []
|
|
|
|
|
|
class TestPrepareSeriesContext:
|
|
"""Test series context preparation for templates."""
|
|
|
|
def test_prepare_series_context_basic(self):
|
|
"""Test preparing basic series context."""
|
|
from src.server.utils.template_helpers import prepare_series_context
|
|
|
|
series_data = [
|
|
{"key": "attack-on-titan", "name": "Attack on Titan", "folder": "AOT"},
|
|
{"key": "one-piece", "name": "One Piece", "folder": "OP"}
|
|
]
|
|
|
|
result = prepare_series_context(series_data)
|
|
|
|
assert len(result) == 2
|
|
assert result[0]["key"] == "attack-on-titan"
|
|
assert result[0]["name"] == "Attack on Titan"
|
|
assert result[0]["folder"] == "AOT"
|
|
|
|
def test_prepare_series_context_sort_by_name(self):
|
|
"""Test preparing series context sorted by name."""
|
|
from src.server.utils.template_helpers import prepare_series_context
|
|
|
|
series_data = [
|
|
{"key": "one-piece", "name": "One Piece", "folder": "OP"},
|
|
{"key": "attack-on-titan", "name": "Attack on Titan", "folder": "AOT"},
|
|
{"key": "death-note", "name": "Death Note", "folder": "DN"}
|
|
]
|
|
|
|
result = prepare_series_context(series_data, sort_by="name")
|
|
|
|
assert result[0]["name"] == "Attack on Titan"
|
|
assert result[1]["name"] == "Death Note"
|
|
assert result[2]["name"] == "One Piece"
|
|
|
|
def test_prepare_series_context_sort_by_key(self):
|
|
"""Test preparing series context sorted by key."""
|
|
from src.server.utils.template_helpers import prepare_series_context
|
|
|
|
series_data = [
|
|
{"key": "z-series", "name": "Z Series", "folder": "Z"},
|
|
{"key": "a-series", "name": "A Series", "folder": "A"},
|
|
{"key": "m-series", "name": "M Series", "folder": "M"}
|
|
]
|
|
|
|
result = prepare_series_context(series_data, sort_by="key")
|
|
|
|
assert result[0]["key"] == "a-series"
|
|
assert result[1]["key"] == "m-series"
|
|
assert result[2]["key"] == "z-series"
|
|
|
|
def test_prepare_series_context_sort_by_folder(self):
|
|
"""Test preparing series context sorted by folder."""
|
|
from src.server.utils.template_helpers import prepare_series_context
|
|
|
|
series_data = [
|
|
{"key": "series1", "name": "Series 1", "folder": "Z Folder"},
|
|
{"key": "series2", "name": "Series 2", "folder": "A Folder"},
|
|
{"key": "series3", "name": "Series 3", "folder": "M Folder"}
|
|
]
|
|
|
|
result = prepare_series_context(series_data, sort_by="folder")
|
|
|
|
assert result[0]["folder"] == "A Folder"
|
|
assert result[1]["folder"] == "M Folder"
|
|
assert result[2]["folder"] == "Z Folder"
|
|
|
|
def test_prepare_series_context_empty_list(self):
|
|
"""Test preparing empty series context."""
|
|
from src.server.utils.template_helpers import prepare_series_context
|
|
|
|
result = prepare_series_context([])
|
|
|
|
assert result == []
|
|
|
|
def test_prepare_series_context_missing_key(self):
|
|
"""Test preparing series context with missing key field."""
|
|
from src.server.utils.template_helpers import prepare_series_context
|
|
|
|
series_data = [
|
|
{"name": "No Key Series", "folder": "NoKey"},
|
|
{"key": "valid-key", "name": "Valid Series", "folder": "Valid"}
|
|
]
|
|
|
|
result = prepare_series_context(series_data)
|
|
|
|
# Only the series with key should be included
|
|
assert len(result) == 1
|
|
assert result[0]["key"] == "valid-key"
|
|
|
|
def test_prepare_series_context_default_values(self):
|
|
"""Test preparing series context fills in default values."""
|
|
from src.server.utils.template_helpers import prepare_series_context
|
|
|
|
series_data = [
|
|
{"key": "minimal-series"} # Missing name and folder
|
|
]
|
|
|
|
result = prepare_series_context(series_data)
|
|
|
|
assert result[0]["key"] == "minimal-series"
|
|
assert result[0]["name"] == "minimal-series" # Should use key as default
|
|
assert result[0]["folder"] == "" # Should default to empty string
|
|
|
|
def test_prepare_series_context_extra_fields(self):
|
|
"""Test preparing series context preserves extra fields."""
|
|
from src.server.utils.template_helpers import prepare_series_context
|
|
|
|
series_data = [
|
|
{
|
|
"key": "series1",
|
|
"name": "Series 1",
|
|
"folder": "S1",
|
|
"year": 2020,
|
|
"status": "completed",
|
|
"episodes": 12
|
|
}
|
|
]
|
|
|
|
result = prepare_series_context(series_data)
|
|
|
|
assert result[0]["year"] == 2020
|
|
assert result[0]["status"] == "completed"
|
|
assert result[0]["episodes"] == 12
|
|
|
|
|
|
class TestGetSeriesByKey:
|
|
"""Test finding series by key."""
|
|
|
|
def test_get_series_by_key_found(self):
|
|
"""Test getting series by key when found."""
|
|
from src.server.utils.template_helpers import get_series_by_key
|
|
|
|
series_data = [
|
|
{"key": "attack-on-titan", "name": "Attack on Titan"},
|
|
{"key": "one-piece", "name": "One Piece"}
|
|
]
|
|
|
|
result = get_series_by_key(series_data, "attack-on-titan")
|
|
|
|
assert result is not None
|
|
assert result["name"] == "Attack on Titan"
|
|
|
|
def test_get_series_by_key_not_found(self):
|
|
"""Test getting series by key when not found."""
|
|
from src.server.utils.template_helpers import get_series_by_key
|
|
|
|
series_data = [
|
|
{"key": "attack-on-titan", "name": "Attack on Titan"}
|
|
]
|
|
|
|
result = get_series_by_key(series_data, "nonexistent")
|
|
|
|
assert result is None
|
|
|
|
def test_get_series_by_key_empty_list(self):
|
|
"""Test getting series by key from empty list."""
|
|
from src.server.utils.template_helpers import get_series_by_key
|
|
|
|
result = get_series_by_key([], "any-key")
|
|
|
|
assert result is None
|
|
|
|
def test_get_series_by_key_case_sensitive(self):
|
|
"""Test that key matching is case-sensitive."""
|
|
from src.server.utils.template_helpers import get_series_by_key
|
|
|
|
series_data = [
|
|
{"key": "Attack-on-Titan", "name": "Attack on Titan"}
|
|
]
|
|
|
|
result = get_series_by_key(series_data, "attack-on-titan")
|
|
|
|
assert result is None # Different case should not match
|
|
|
|
|
|
class TestFilterSeriesByMissingEpisodes:
|
|
"""Test filtering series by missing episodes."""
|
|
|
|
def test_filter_series_with_missing_episodes(self):
|
|
"""Test filtering series with missing episodes."""
|
|
from src.server.utils.template_helpers import filter_series_by_missing_episodes
|
|
|
|
series_data = [
|
|
{
|
|
"key": "series1",
|
|
"name": "Series 1",
|
|
"missing_episodes": {"season_1": [1, 2, 3]}
|
|
},
|
|
{
|
|
"key": "series2",
|
|
"name": "Series 2",
|
|
"missing_episodes": {"season_1": []}
|
|
},
|
|
{
|
|
"key": "series3",
|
|
"name": "Series 3",
|
|
"missing_episodes": {"season_1": [5]}
|
|
}
|
|
]
|
|
|
|
result = filter_series_by_missing_episodes(series_data)
|
|
|
|
assert len(result) == 2
|
|
assert result[0]["key"] == "series1"
|
|
assert result[1]["key"] == "series3"
|
|
|
|
def test_filter_series_no_missing_episodes(self):
|
|
"""Test filtering when no series have missing episodes."""
|
|
from src.server.utils.template_helpers import filter_series_by_missing_episodes
|
|
|
|
series_data = [
|
|
{
|
|
"key": "series1",
|
|
"name": "Series 1",
|
|
"missing_episodes": {"season_1": []}
|
|
},
|
|
{
|
|
"key": "series2",
|
|
"name": "Series 2",
|
|
"missing_episodes": {}
|
|
}
|
|
]
|
|
|
|
result = filter_series_by_missing_episodes(series_data)
|
|
|
|
assert len(result) == 0
|
|
|
|
def test_filter_series_empty_list(self):
|
|
"""Test filtering empty series list."""
|
|
from src.server.utils.template_helpers import filter_series_by_missing_episodes
|
|
|
|
result = filter_series_by_missing_episodes([])
|
|
|
|
assert len(result) == 0
|
|
|
|
def test_filter_series_missing_field(self):
|
|
"""Test filtering when missing_episodes field is missing."""
|
|
from src.server.utils.template_helpers import filter_series_by_missing_episodes
|
|
|
|
series_data = [
|
|
{"key": "series1", "name": "Series 1"} # No missing_episodes
|
|
]
|
|
|
|
result = filter_series_by_missing_episodes(series_data)
|
|
|
|
# Should not crash and should not include this series
|
|
assert len(result) == 0
|
|
|
|
def test_filter_series_multiple_seasons(self):
|
|
"""Test filtering with multiple seasons."""
|
|
from src.server.utils.template_helpers import filter_series_by_missing_episodes
|
|
|
|
series_data = [
|
|
{
|
|
"key": "series1",
|
|
"name": "Series 1",
|
|
"missing_episodes": {
|
|
"season_1": [1, 2],
|
|
"season_2": []
|
|
}
|
|
}
|
|
]
|
|
|
|
result = filter_series_by_missing_episodes(series_data)
|
|
|
|
# Should include because season_1 has missing episodes
|
|
assert len(result) == 1
|