From 30b1323a426d172569ec5adab761e3b78833d915 Mon Sep 17 00:00:00 2001 From: Pascal Repond Date: Wed, 21 Jan 2026 20:54:44 +0100 Subject: [PATCH] test: add tests for latest features Also harmonises back buttons across the application. --- src/templates/accounts/profile_edit.html | 8 +- src/templates/base/backup_manage.html | 14 +-- src/templates/base/media_detail.html | 5 +- src/templates/base/media_edit.html | 5 +- src/templates/base/media_import.html | 8 +- .../partials/common/back_button.html | 8 ++ src/tests/core/test_context_processors.py | 84 ++++++++++++++++++ src/tests/core/test_musicbrainz.py | 86 +++++++++++++++++++ 8 files changed, 193 insertions(+), 25 deletions(-) create mode 100644 src/templates/partials/common/back_button.html create mode 100644 src/tests/core/test_context_processors.py create mode 100644 src/tests/core/test_musicbrainz.py diff --git a/src/templates/accounts/profile_edit.html b/src/templates/accounts/profile_edit.html index e840717..2065bab 100644 --- a/src/templates/accounts/profile_edit.html +++ b/src/templates/accounts/profile_edit.html @@ -5,7 +5,10 @@ {% endblock title %} {% block content %}
-

{% translate "Edit Profile" %}

+
+ {% include "partials/common/back_button.html" %} +

{% translate "Edit Profile" %}

+
@@ -140,8 +143,5 @@

{% translate "Change Password" %}

-
{% endblock content %} diff --git a/src/templates/base/backup_manage.html b/src/templates/base/backup_manage.html index bf94838..f6d8c95 100644 --- a/src/templates/base/backup_manage.html +++ b/src/templates/base/backup_manage.html @@ -6,7 +6,10 @@ {% endblock title %} {% block content %}
-

{% translate "Backup Management" %}

+
+ {% include "partials/common/back_button.html" %} +

{% translate "Backup Management" %}

+
{% if messages %}
@@ -101,15 +104,6 @@

- -
- -
{# Confirmation modal for backup import #} {% include "partials/common/confirm_modal.html" with modal_id="confirm-import-modal" title=_("⚠️ WARNING: Destructive Action") message=_("This action will DELETE ALL your current data and replace it with the backup data. Are you absolutely sure you want to continue?") confirm_text=_("Yes, import backup") is_danger=True form_id="import-backup-form" %} diff --git a/src/templates/base/media_detail.html b/src/templates/base/media_detail.html index 2f11d0f..c6b688f 100644 --- a/src/templates/base/media_detail.html +++ b/src/templates/base/media_detail.html @@ -9,10 +9,7 @@ {# Header #}
- + {% include "partials/common/back_button.html" %}

{{ media.title }} {% if media.pub_year %}({{ media.pub_year }}){% endif %} diff --git a/src/templates/base/media_edit.html b/src/templates/base/media_edit.html index dba3e49..71e9286 100644 --- a/src/templates/base/media_edit.html +++ b/src/templates/base/media_edit.html @@ -25,10 +25,7 @@ {# Header #}
- + {% include "partials/common/back_button.html" %}

{% if media %} {% translate "Edit" %} {{ media.title }} diff --git a/src/templates/base/media_import.html b/src/templates/base/media_import.html index e5ecae9..57af752 100644 --- a/src/templates/base/media_import.html +++ b/src/templates/base/media_import.html @@ -7,9 +7,7 @@
{# Header #}
- {% lucide "arrow-left" %} + {% include "partials/common/back_button.html" %}

{% translate "Import metadata" %}

{# Source selector tabs #} @@ -20,24 +18,28 @@

{% translate "Import metadata" %}

aria-label="{% translate 'Movies & TV' %}" id="tab-tmdb" checked + autocomplete="off" hx-on:click="document.getElementById('source-tmdb').classList.remove('hidden'); document.getElementById('source-igdb').classList.add('hidden'); document.getElementById('source-openlibrary').classList.add('hidden'); document.getElementById('source-musicbrainz').classList.add('hidden'); document.getElementById('import-results').innerHTML='';">
{# TMDB Search (Movies & TV) #} diff --git a/src/templates/partials/common/back_button.html b/src/templates/partials/common/back_button.html new file mode 100644 index 0000000..e33eb8c --- /dev/null +++ b/src/templates/partials/common/back_button.html @@ -0,0 +1,8 @@ +{% load i18n %} +{% load lucide %} + diff --git a/src/tests/core/test_context_processors.py b/src/tests/core/test_context_processors.py new file mode 100644 index 0000000..85296ae --- /dev/null +++ b/src/tests/core/test_context_processors.py @@ -0,0 +1,84 @@ +""" +Tests for core.context_processors module. + +These tests verify the behavior of context processors. +""" + +from django.contrib.auth.models import AnonymousUser +from django.test import RequestFactory + +from core.context_processors import saved_views +from core.models import SavedView + + +class TestSavedViewsContextProcessor: + """Tests for the saved_views context processor.""" + + def test_authenticated_user_gets_their_saved_views(self, user, db): + """Authenticated users receive their saved views in context.""" + # Create saved views for the user + SavedView.objects.create(user=user, name="View 1") + SavedView.objects.create(user=user, name="View 2") + + # Create a mock request with the authenticated user + factory = RequestFactory() + request = factory.get("/") + request.user = user + + result = saved_views(request) + + assert "saved_views" in result + assert result["saved_views"].count() == 2 + + def test_authenticated_user_only_gets_own_views(self, user, django_user_model, db): + """Users only receive their own saved views, not others'.""" + # Create another user with saved views + other_user = django_user_model.objects.create_user( + username="otheruser", + email="other@example.com", + password="testpass123", + ) + SavedView.objects.create(user=other_user, name="Other View") + SavedView.objects.create(user=user, name="My View") + + factory = RequestFactory() + request = factory.get("/") + request.user = user + + result = saved_views(request) + + assert result["saved_views"].count() == 1 + assert result["saved_views"].first().name == "My View" + + def test_anonymous_user_gets_empty_queryset(self, db): + """Anonymous users receive an empty queryset.""" + + factory = RequestFactory() + request = factory.get("/") + request.user = AnonymousUser() + + result = saved_views(request) + + assert "saved_views" in result + assert result["saved_views"].count() == 0 + + def test_saved_views_available_on_all_pages(self, logged_in_client, user, db): + """Saved views are available in context on various pages.""" + from django.urls import reverse + + SavedView.objects.create(user=user, name="Test View") + + # Test home page + response = logged_in_client.get(reverse("home")) + assert "saved_views" in response.context + assert response.context["saved_views"].count() == 1 + + # Test import page + response = logged_in_client.get(reverse("media_import")) + assert "saved_views" in response.context + assert response.context["saved_views"].count() == 1 + + # Test backup manage page + response = logged_in_client.get(reverse("backup_manage")) + assert "saved_views" in response.context + assert response.context["saved_views"].count() == 1 diff --git a/src/tests/core/test_musicbrainz.py b/src/tests/core/test_musicbrainz.py new file mode 100644 index 0000000..ff79157 --- /dev/null +++ b/src/tests/core/test_musicbrainz.py @@ -0,0 +1,86 @@ +""" +Tests for MusicBrainz integration in views. + +These tests verify the application behavior, not the external API. +""" + +from unittest.mock import MagicMock, patch + +import requests +from django.urls import reverse + +from core.services.musicbrainz import MusicBrainzResult + + +class TestMusicBrainzSearchView: + """Tests for the musicbrainz_search_htmx view.""" + + def test_returns_empty_for_short_query(self, logged_in_client): + """Returns empty results for queries shorter than minimum length.""" + response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": "a"}) + + assert response.status_code == 200 + assert "partials/musicbrainz/musicbrainz_suggestions.html" in [t.name for t in response.templates] + assert response.context["results"] == [] + + def test_returns_empty_for_empty_query(self, logged_in_client): + """Returns empty results for empty query.""" + response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": ""}) + + assert response.status_code == 200 + assert response.context["results"] == [] + + @patch("core.views.get_musicbrainz_client") + def test_returns_search_results(self, mock_get_client, logged_in_client): + """Returns search results from MusicBrainz client.""" + mock_client = MagicMock() + mock_client.search_releases.return_value = [ + MusicBrainzResult( + mbid="test-mbid-123", + title="Abbey Road", + artists=["The Beatles"], + year=1969, + country="GB", + label="Apple Records", + ) + ] + mock_get_client.return_value = mock_client + + response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": "abbey road"}) + + assert response.status_code == 200 + assert len(response.context["results"]) == 1 + assert response.context["results"][0].title == "Abbey Road" + mock_client.search_releases.assert_called_once() + + @patch("core.views.get_musicbrainz_client") + def test_handles_api_error_gracefully(self, mock_get_client, logged_in_client): + """Handles API errors gracefully and shows error message.""" + mock_client = MagicMock() + mock_client.search_releases.side_effect = requests.RequestException("API Error") + mock_get_client.return_value = mock_client + + response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": "test query"}) + + assert response.status_code == 200 + assert "error" in response.context + assert response.context["error"] == "Search failed" + + def test_preserves_media_id_in_context(self, logged_in_client): + """Preserves media_id in context for editing existing media.""" + response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": "", "media_id": "42"}) + + assert response.status_code == 200 + assert response.context["media_id"] == "42" + + @patch("core.views.get_musicbrainz_client") + def test_preserves_query_in_context(self, mock_get_client, logged_in_client): + """Preserves search query in context.""" + mock_client = MagicMock() + mock_client.search_releases.return_value = [] + mock_get_client.return_value = mock_client + + response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": "test"}) + + assert response.status_code == 200 + assert response.context["query"] == "test"