Skip to content

Commit b013b4f

Browse files
committed
test: add tests for latest features
Also harmonises back buttons across the application.
1 parent 1023487 commit b013b4f

8 files changed

Lines changed: 193 additions & 25 deletions

File tree

src/templates/accounts/profile_edit.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
{% endblock title %}
66
{% block content %}
77
<div class="max-w-2xl mx-auto px-4 py-8">
8-
<h1 class="text-3xl font-bold mb-8">{% translate "Edit Profile" %}</h1>
8+
<div class="flex items-center gap-4 mb-8">
9+
{% include "partials/common/back_button.html" %}
10+
<h1 class="text-4xl font-bold">{% translate "Edit Profile" %}</h1>
11+
</div>
912
<!-- Profile Information -->
1013
<div class="card bg-base-200 shadow-xl mb-8">
1114
<div class="card-body">
@@ -140,8 +143,5 @@ <h2 class="card-title">{% translate "Change Password" %}</h2>
140143
</form>
141144
</div>
142145
</div>
143-
<div class="mt-8">
144-
<a href="{% url 'home' %}" class="btn btn-ghost">← {% translate "Back to Home" %}</a>
145-
</div>
146146
</div>
147147
{% endblock content %}

src/templates/base/backup_manage.html

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
{% endblock title %}
77
{% block content %}
88
<div class="max-w-4xl mx-auto">
9-
<h1 class="text-3xl font-bold mb-8">{% translate "Backup Management" %}</h1>
9+
<div class="flex items-center gap-4 mb-8">
10+
{% include "partials/common/back_button.html" %}
11+
<h1 class="text-4xl font-bold">{% translate "Backup Management" %}</h1>
12+
</div>
1013
<!-- Messages -->
1114
{% if messages %}
1215
<div class="mb-6">
@@ -101,15 +104,6 @@ <h2 class="card-title text-warning">
101104
</div>
102105
</div>
103106
</div>
104-
<!-- Back Button -->
105-
<div class="mt-6">
106-
<button type="button"
107-
onclick="history.length> 1 ? history.back() : window.location.href='{% url 'home' %}'"
108-
class="btn btn-ghost">
109-
{% lucide "arrow-left" %}
110-
{% translate "Back to Home" %}
111-
</button>
112-
</div>
113107
</div>
114108
{# Confirmation modal for backup import #}
115109
{% 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" %}

src/templates/base/media_detail.html

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@
99
{# Header #}
1010
<div class="flex justify-between items-start gap-4 mb-6">
1111
<div class="flex-1">
12-
<button type="button"
13-
onclick="history.length > 1 ? history.back() : window.location.href='{% url 'home' %}'"
14-
class="btn btn-ghost btn-sm btn-circle"
15-
aria-label="{% translate "Back" %}">{% lucide "arrow-left" %}</button>
12+
{% include "partials/common/back_button.html" %}
1613
<h1 class="text-4xl font-bold">
1714
{{ media.title }}
1815
{% if media.pub_year %}<span class="text-2xl opacity-70 font-normal">({{ media.pub_year }})</span>{% endif %}

src/templates/base/media_edit.html

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@
2525
{# Header #}
2626
<div class="flex justify-between items-start gap-4 mb-6">
2727
<div class="flex-1">
28-
<button type="button"
29-
onclick="history.length > 1 ? history.back() : window.location.href='{% url 'home' %}'"
30-
class="btn btn-ghost btn-sm btn-circle"
31-
aria-label="{% translate "Back" %}">{% lucide "arrow-left" %}</button>
28+
{% include "partials/common/back_button.html" %}
3229
<h1 class="text-4xl font-bold">
3330
{% if media %}
3431
{% translate "Edit" %} {{ media.title }}

src/templates/base/media_import.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77
<div class="max-w-2xl mx-auto">
88
{# Header #}
99
<div class="flex items-center gap-4 mb-6">
10-
<a href="{% if media_id %}{% url 'media_edit' media_id %}{% else %}{% url 'media_add' %}{% endif %}"
11-
class="btn btn-ghost btn-sm btn-circle"
12-
aria-label="{% translate 'Back' %}">{% lucide "arrow-left" %}</a>
10+
{% include "partials/common/back_button.html" %}
1311
<h1 class="text-4xl font-bold">{% translate "Import metadata" %}</h1>
1412
</div>
1513
{# Source selector tabs #}
@@ -20,24 +18,28 @@ <h1 class="text-4xl font-bold">{% translate "Import metadata" %}</h1>
2018
aria-label="{% translate 'Movies & TV' %}"
2119
id="tab-tmdb"
2220
checked
21+
autocomplete="off"
2322
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='';">
2423
<input type="radio"
2524
name="import_source"
2625
class="tab"
2726
aria-label="{% translate 'Video games' %}"
2827
id="tab-igdb"
28+
autocomplete="off"
2929
hx-on:click="document.getElementById('source-igdb').classList.remove('hidden'); document.getElementById('source-tmdb').classList.add('hidden'); document.getElementById('source-openlibrary').classList.add('hidden'); document.getElementById('source-musicbrainz').classList.add('hidden'); document.getElementById('import-results').innerHTML='';">
3030
<input type="radio"
3131
name="import_source"
3232
class="tab"
3333
aria-label="{% translate 'Books' %}"
3434
id="tab-openlibrary"
35+
autocomplete="off"
3536
hx-on:click="document.getElementById('source-openlibrary').classList.remove('hidden'); document.getElementById('source-tmdb').classList.add('hidden'); document.getElementById('source-igdb').classList.add('hidden'); document.getElementById('source-musicbrainz').classList.add('hidden'); document.getElementById('import-results').innerHTML='';">
3637
<input type="radio"
3738
name="import_source"
3839
class="tab"
3940
aria-label="{% translate 'Music' %}"
4041
id="tab-musicbrainz"
42+
autocomplete="off"
4143
hx-on:click="document.getElementById('source-musicbrainz').classList.remove('hidden'); document.getElementById('source-tmdb').classList.add('hidden'); document.getElementById('source-igdb').classList.add('hidden'); document.getElementById('source-openlibrary').classList.add('hidden'); document.getElementById('import-results').innerHTML='';">
4244
</div>
4345
{# TMDB Search (Movies & TV) #}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% load i18n %}
2+
{% load lucide %}
3+
<button type="button"
4+
onclick="history.length > 1 ? history.back() : window.location.href='{% url 'home' %}'"
5+
class="btn btn-ghost btn-sm btn-circle"
6+
aria-label="{% translate 'Back' %}">
7+
{% lucide "arrow-left" %}
8+
</button>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""
2+
Tests for core.context_processors module.
3+
4+
These tests verify the behavior of context processors.
5+
"""
6+
7+
from django.contrib.auth.models import AnonymousUser
8+
from django.test import RequestFactory
9+
10+
from core.context_processors import saved_views
11+
from core.models import SavedView
12+
13+
14+
class TestSavedViewsContextProcessor:
15+
"""Tests for the saved_views context processor."""
16+
17+
def test_authenticated_user_gets_their_saved_views(self, user, db):
18+
"""Authenticated users receive their saved views in context."""
19+
# Create saved views for the user
20+
SavedView.objects.create(user=user, name="View 1")
21+
SavedView.objects.create(user=user, name="View 2")
22+
23+
# Create a mock request with the authenticated user
24+
factory = RequestFactory()
25+
request = factory.get("/")
26+
request.user = user
27+
28+
result = saved_views(request)
29+
30+
assert "saved_views" in result
31+
assert result["saved_views"].count() == 2
32+
33+
def test_authenticated_user_only_gets_own_views(self, user, django_user_model, db):
34+
"""Users only receive their own saved views, not others'."""
35+
# Create another user with saved views
36+
other_user = django_user_model.objects.create_user(
37+
username="otheruser",
38+
email="other@example.com",
39+
password="testpass123",
40+
)
41+
SavedView.objects.create(user=other_user, name="Other View")
42+
SavedView.objects.create(user=user, name="My View")
43+
44+
factory = RequestFactory()
45+
request = factory.get("/")
46+
request.user = user
47+
48+
result = saved_views(request)
49+
50+
assert result["saved_views"].count() == 1
51+
assert result["saved_views"].first().name == "My View"
52+
53+
def test_anonymous_user_gets_empty_queryset(self, db):
54+
"""Anonymous users receive an empty queryset."""
55+
56+
factory = RequestFactory()
57+
request = factory.get("/")
58+
request.user = AnonymousUser()
59+
60+
result = saved_views(request)
61+
62+
assert "saved_views" in result
63+
assert result["saved_views"].count() == 0
64+
65+
def test_saved_views_available_on_all_pages(self, logged_in_client, user, db):
66+
"""Saved views are available in context on various pages."""
67+
from django.urls import reverse
68+
69+
SavedView.objects.create(user=user, name="Test View")
70+
71+
# Test home page
72+
response = logged_in_client.get(reverse("home"))
73+
assert "saved_views" in response.context
74+
assert response.context["saved_views"].count() == 1
75+
76+
# Test import page
77+
response = logged_in_client.get(reverse("media_import"))
78+
assert "saved_views" in response.context
79+
assert response.context["saved_views"].count() == 1
80+
81+
# Test backup manage page
82+
response = logged_in_client.get(reverse("backup_manage"))
83+
assert "saved_views" in response.context
84+
assert response.context["saved_views"].count() == 1

src/tests/core/test_musicbrainz.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""
2+
Tests for MusicBrainz integration in views.
3+
4+
These tests verify the application behavior, not the external API.
5+
"""
6+
7+
from unittest.mock import MagicMock, patch
8+
9+
import requests
10+
from django.urls import reverse
11+
12+
from core.services.musicbrainz import MusicBrainzResult
13+
14+
15+
class TestMusicBrainzSearchView:
16+
"""Tests for the musicbrainz_search_htmx view."""
17+
18+
def test_returns_empty_for_short_query(self, logged_in_client):
19+
"""Returns empty results for queries shorter than minimum length."""
20+
response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": "a"})
21+
22+
assert response.status_code == 200
23+
assert "partials/musicbrainz/musicbrainz_suggestions.html" in [t.name for t in response.templates]
24+
assert response.context["results"] == []
25+
26+
def test_returns_empty_for_empty_query(self, logged_in_client):
27+
"""Returns empty results for empty query."""
28+
response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": ""})
29+
30+
assert response.status_code == 200
31+
assert response.context["results"] == []
32+
33+
@patch("core.views.get_musicbrainz_client")
34+
def test_returns_search_results(self, mock_get_client, logged_in_client):
35+
"""Returns search results from MusicBrainz client."""
36+
mock_client = MagicMock()
37+
mock_client.search_releases.return_value = [
38+
MusicBrainzResult(
39+
mbid="test-mbid-123",
40+
title="Abbey Road",
41+
artists=["The Beatles"],
42+
year=1969,
43+
country="GB",
44+
label="Apple Records",
45+
)
46+
]
47+
mock_get_client.return_value = mock_client
48+
49+
response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": "abbey road"})
50+
51+
assert response.status_code == 200
52+
assert len(response.context["results"]) == 1
53+
assert response.context["results"][0].title == "Abbey Road"
54+
mock_client.search_releases.assert_called_once()
55+
56+
@patch("core.views.get_musicbrainz_client")
57+
def test_handles_api_error_gracefully(self, mock_get_client, logged_in_client):
58+
"""Handles API errors gracefully and shows error message."""
59+
mock_client = MagicMock()
60+
mock_client.search_releases.side_effect = requests.RequestException("API Error")
61+
mock_get_client.return_value = mock_client
62+
63+
response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": "test query"})
64+
65+
assert response.status_code == 200
66+
assert "error" in response.context
67+
assert response.context["error"] == "Search failed"
68+
69+
def test_preserves_media_id_in_context(self, logged_in_client):
70+
"""Preserves media_id in context for editing existing media."""
71+
response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": "", "media_id": "42"})
72+
73+
assert response.status_code == 200
74+
assert response.context["media_id"] == "42"
75+
76+
@patch("core.views.get_musicbrainz_client")
77+
def test_preserves_query_in_context(self, mock_get_client, logged_in_client):
78+
"""Preserves search query in context."""
79+
mock_client = MagicMock()
80+
mock_client.search_releases.return_value = []
81+
mock_get_client.return_value = mock_client
82+
83+
response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": "test"})
84+
85+
assert response.status_code == 200
86+
assert response.context["query"] == "test"

0 commit comments

Comments
 (0)