From 87086cdce9c9d694e40373afaca46b4a75ab0ce2 Mon Sep 17 00:00:00 2001 From: Michal Kunc Date: Sat, 11 Feb 2023 22:40:16 +0100 Subject: [PATCH] Add tests and fix bugs --- poetry.lock | 14 ++- pyproject.toml | 3 + templates/404.html | 4 +- templates/watchlist/index.html | 10 +- watchlist/tests/test_views.py | 175 +++++++++++++++++++++++++++++++++ watchlist/views.py | 2 + 6 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 watchlist/tests/test_views.py diff --git a/poetry.lock b/poetry.lock index e9a3024..e2c4d5e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -181,6 +181,18 @@ files = [ {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"}, ] +[[package]] +name = "tblib" +version = "1.7.0" +description = "Traceback serialization library." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "tblib-1.7.0-py2.py3-none-any.whl", hash = "sha256:289fa7359e580950e7d9743eab36b0691f0310fce64dee7d9c31065b8f723e23"}, + {file = "tblib-1.7.0.tar.gz", hash = "sha256:059bd77306ea7b419d4f76016aef6d7027cc8a0785579b5aad198803435f882c"}, +] + [[package]] name = "tzdata" version = "2022.7" @@ -223,4 +235,4 @@ brotli = ["Brotli"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c0a2d81dcf191d69565b3de08983d96c7f5ad00a6de9eba5b350b1431a5e99b3" +content-hash = "ca22a0efd5d4acfb0a4a09e8567d0a8c90debca1203b7a80262919d8055366da" diff --git a/pyproject.toml b/pyproject.toml index 90f6f4a..3e5586c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,9 @@ gunicorn = "^20.1.0" whitenoise = "^6.3.0" +[tool.poetry.group.dev.dependencies] +tblib = "^1.7.0" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/templates/404.html b/templates/404.html index eccc3b3..b11bc6f 100644 --- a/templates/404.html +++ b/templates/404.html @@ -1,4 +1,6 @@ {% extends "base.html" %} {% block content %} -

Nothing here...

+
+

Nothing here...

+
{% endblock %} \ No newline at end of file diff --git a/templates/watchlist/index.html b/templates/watchlist/index.html index a5201d9..0dbedc7 100644 --- a/templates/watchlist/index.html +++ b/templates/watchlist/index.html @@ -1,19 +1,23 @@ {% extends 'base.html' %} {% block content %}
-

Watchlist

+

Watchlist

+ {% if object_list %} + {% else %} +

No movies yet.

+ {% endif %} {% if can_add_movie %}

Submit new movie

{% csrf_token %} - +
{% endif %}
diff --git a/watchlist/tests/test_views.py b/watchlist/tests/test_views.py new file mode 100644 index 0000000..291e08f --- /dev/null +++ b/watchlist/tests/test_views.py @@ -0,0 +1,175 @@ +from django.test import TestCase +from django.urls import reverse +from django.contrib.auth.models import User, Permission +from django.contrib.auth.hashers import make_password + +from watchlist.models import Movie, MovieVote + +def create_user(name="user", is_staff=False): + user = User.objects.create(username=name, password=make_password("dummy")) + user.user_permissions.add(Permission.objects.get(codename="add_movie")) + user.save() + return user + +def create_movie(name="Test movie", added_by="user"): + return Movie.objects.create(name=name, suggested_by=User.objects.filter(username=added_by).first(), watched=False) + +class IndexViewTests(TestCase): + + def setUp(self): + self.user = create_user("user") + + def test_no_movies(self): + """Tests the index page with no movies""" + response = self.client.get(reverse('watchlist:index')) + self.assertQuerysetEqual(response.context["object_list"], []) + self.assertContains(response, "No movies yet.") + self.assertNotContains(response, "Submit new movie") + + def test_no_movies_logged_in(self): + """Tests the index page with no movies, while the user is logged in.""" + self.client.login(username="user", password="dummy") + response = self.client.get(reverse('watchlist:index')) + self.assertQuerysetEqual(response.context["object_list"], []) + self.assertContains(response, "No movies yet.") + self.assertContains(response, "Submit new movie") + + def test_with_movie(self): + """Tests that the index page shows a movie, hides it if it watched, but shows + it with proper filter""" + create_movie() + with self.subTest(watched=False): + response = self.client.get(reverse('watchlist:index')) + self.assertQuerysetEqual(response.context["object_list"], Movie.objects.all()) + self.assertContains(response, "Test movie") + movie = Movie.objects.first() + movie.watched = True + movie.save() + with self.subTest(watched=True, filter=False): + response = self.client.get(reverse('watchlist:index')) + self.assertQuerysetEqual(response.context["object_list"], []) + self.assertNotContains(response, "Test movie") + with self.subTest(watched=True, filter=True): + response = self.client.get(reverse('watchlist:index',) + "?watched") + self.assertQuerysetEqual(response.context["object_list"], Movie.objects.all()) + self.assertContains(response, "Test movie") + + def test_unvoted(self): + """Test that the index shows an asterisk with movies that the user hasn't voted on yet""" + create_movie() + self.client.login(username="user",password="dummy") + response = self.client.get(reverse('watchlist:index')) + self.assertContains(response, "Test movie*") + + def test_voted(self): + """Test that the index doesn't show an asterisk with movies the has voted on.""" + movie = create_movie() + MovieVote.objects.create(movie=movie, user=self.user, vote=1) + self.client.login(username="user",password="dummy") + response = self.client.get(reverse('watchlist:index')) + self.assertContains(response, "Test movie") + self.assertNotContains(response, "Test movie*") + + + def test_movie_sorting(self): + """Test various methods of movie sorting""" + m1 = create_movie(name="ZZZ: A movie test") + m2 = create_movie(name="Test movie 2") + tests = [ + ('?sort=id', [m1,m2]), + ('?sort=-id', [m2,m1]), + ('?sort=name', [m2,m1]), + ('?sort=-name', [m1,m2]), + ] + for param, qs in tests: + with self.subTest(param=param, qs=qs): + response = self.client.get(reverse('watchlist:index') + param) + self.assertQuerysetEqual(response.context["object_list"], qs) + self.assertContains(response, m1.name) + self.assertContains(response, m2.name) + +class MovieDetailViewTests(TestCase): + + def setUp(self): + self.user = create_user("user") + + def test_detail_logged_out(self): + m = create_movie() + response = self.client.get(reverse('watchlist:detail', args=(m.id,))) + self.assertContains(response, m.name) + self.assertEqual(response.context["movie"], m) + self.assertNotContains(response, "Edit") + + def test_detail_logged_in(self): + m = create_movie() + self.client.login(username="user", password="dummy") + response = self.client.get(reverse('watchlist:detail', args=(m.id,))) + self.assertContains(response, m.name) + self.assertEqual(response.context["movie"], m) + self.assertContains(response, "Edit") + + def test_no_movie(self): + response = self.client.get(reverse('watchlist:detail', args=(1,))) + self.assertEqual(response.status_code, 404) + +class VoteTests(TestCase): + + def setUp(self): + self.user = create_user("user") + + def test_no_user_voting(self): + m = create_movie() + response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"vote": 1, "seen": True}) + self.assertRedirects(response, "/accounts/login/?next=" + response.request["PATH_INFO"], fetch_redirect_response=False) + + def test_user_voting(self): + m = create_movie() + score = m.score + self.client.login(username="user", password="dummy") + for vote in [1, 0, -1]: + for seen in True, False: + with self.subTest(vote=vote, seen=seen): + response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"vote": str(vote), "seen": True}) + self.assertRedirects(response, reverse('watchlist:index')) + self.assertEqual(m.score, score + vote) + + def test_invalid_votes(self): + m = create_movie() + score = m.score + self.client.login(username="user", password="dummy") + for vote in [2,3,4,-5,0.5,'abc','']: + with self.subTest(vote=vote): + response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"vote": str(vote), "seen": True}) + self.assertEqual(response.status_code, 400) + self.assertEqual(m.score, score) + with self.subTest(vote="empty"): + response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"seen": True}) + self.assertEqual(response.status_code, 400) + self.assertEqual(m.score, score) + + def test_seen(self): + m = create_movie() + self.client.login(username="user", password="dummy") + for seen in (True, False) * 2: + with self.subTest(seen=seen): + data = {"vote": "0", "seen": "on"} if seen else {"vote": "0"} + response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data=data) + self.assertRedirects(response, reverse('watchlist:index')) + mv = m.movievote_set.get(user=self.user) + self.assertEqual(mv.seen, seen) + + + def test_comment(self): + m = create_movie() + self.client.login(username="user", password="dummy") + for comment in ["aaa", "", "TEST"]: + with self.subTest(comment=comment): + response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"vote": "0", "comment": comment}) + mv = m.movievote_set.get(user=self.user) + self.assertEqual(mv.comment, None if comment == "" else comment) + with self.subTest(comment=None): + response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"vote": "0"}) + mv = m.movievote_set.get(user=self.user) + self.assertEqual(mv.comment, None) + + diff --git a/watchlist/views.py b/watchlist/views.py index 6c138e9..99ba2a0 100644 --- a/watchlist/views.py +++ b/watchlist/views.py @@ -51,6 +51,8 @@ def vote(request, pk): user_vote = movie.movievote_set.filter(user=request.user).first() if user_vote is None: user_vote = models.MovieVote(movie=movie, user=request.user) + if not request.POST.get("vote", None) in ("1", "0", "-1"): + return HttpResponseBadRequest("Invalid vote.") user_vote.vote = request.POST['vote'] user_vote.seen = request.POST.get('seen', False) == "on" comment = request.POST.get('comment', '').strip()