Task 6: Page Controller Tests - 37 tests, 100% coverage

- 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
This commit is contained in:
2026-01-26 18:45:21 +01:00
parent 0ffcfac674
commit ab1836575e
2 changed files with 578 additions and 0 deletions

BIN
.coverage

Binary file not shown.

View File

@@ -0,0 +1,578 @@
"""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