From 8f0667d160c288990318dbab41a1ba2684dfc54d Mon Sep 17 00:00:00 2001 From: Boris Date: Tue, 31 Mar 2026 21:52:26 +0300 Subject: [PATCH] add tests --- .woodpecker.yml | 10 ++ backlogger/tests.py | 283 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 backlogger/tests.py diff --git a/.woodpecker.yml b/.woodpecker.yml index 526d036..f8bef9e 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -3,6 +3,15 @@ when: event: [push, manual] steps: + test: + image: python:3.12-slim + commands: + - pip install --quiet -r requirements.txt + - python manage.py test backlogger --verbosity=2 + environment: + DJANGO_SECRET_KEY: ci-test-key + DATABASE_URL: sqlite:///tmp/test.db + build-and-deploy: image: docker:cli volumes: @@ -13,3 +22,4 @@ steps: - cp -r . /opt/services/app/ - docker build -t k-boris-website /opt/services/app/ - docker compose -f /opt/services/docker-compose.yml up -d --no-deps django + depends_on: [test] diff --git a/backlogger/tests.py b/backlogger/tests.py new file mode 100644 index 0000000..3b4b9c3 --- /dev/null +++ b/backlogger/tests.py @@ -0,0 +1,283 @@ +from django.contrib.auth.models import User +from django.test import TestCase +from django.urls import reverse + +from .forms import ItemForm, SignupForm +from .models import Item + + +def make_user(username='alice', active=True): + user = User.objects.create_user(username=username, password='testpass123', email=f'{username}@example.com') + user.is_active = active + user.save() + return user + + +def make_item(user, name='Test Game', category=Item.GAMES, **kwargs): + return Item.objects.create(user=user, name=name, category=category, **kwargs) + + +# --------------------------------------------------------------------------- +# Auth +# --------------------------------------------------------------------------- + +class AuthRequiredTests(TestCase): + def test_list_redirects_anonymous(self): + response = self.client.get(reverse('backlogger:list')) + self.assertRedirects(response, '/accounts/login/?next=/backlogger/') + + def test_add_redirects_anonymous(self): + response = self.client.get(reverse('backlogger:add')) + self.assertEqual(response.status_code, 302) + + def test_inactive_user_cannot_login(self): + make_user('bob', active=False) + response = self.client.post(reverse('login'), {'username': 'bob', 'password': 'testpass123'}) + # Should stay on login page, not redirect to backlogger + self.assertEqual(response.status_code, 200) + self.assertFalse(response.wsgi_request.user.is_authenticated) + + def test_active_user_can_login(self): + make_user('alice') + response = self.client.post(reverse('login'), {'username': 'alice', 'password': 'testpass123'}, follow=True) + self.assertTrue(response.wsgi_request.user.is_authenticated) + + +# --------------------------------------------------------------------------- +# Signup +# --------------------------------------------------------------------------- + +class SignupTests(TestCase): + def test_get_signup_page(self): + response = self.client.get(reverse('signup')) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Request account') + + def test_signup_creates_inactive_user(self): + response = self.client.post(reverse('signup'), { + 'username': 'newuser', + 'email': 'new@example.com', + 'password1': 'StrongPass99!', + 'password2': 'StrongPass99!', + }) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'pending') + user = User.objects.get(username='newuser') + self.assertFalse(user.is_active) + + def test_signup_invalid_mismatched_passwords(self): + self.client.post(reverse('signup'), { + 'username': 'newuser', + 'email': 'new@example.com', + 'password1': 'StrongPass99!', + 'password2': 'WrongPass99!', + }) + self.assertFalse(User.objects.filter(username='newuser').exists()) + + def test_signup_duplicate_username(self): + make_user('taken') + response = self.client.post(reverse('signup'), { + 'username': 'taken', + 'email': 'other@example.com', + 'password1': 'StrongPass99!', + 'password2': 'StrongPass99!', + }) + self.assertEqual(response.status_code, 200) + self.assertEqual(User.objects.filter(username='taken').count(), 1) + + +# --------------------------------------------------------------------------- +# Item list +# --------------------------------------------------------------------------- + +class ItemListTests(TestCase): + def setUp(self): + self.alice = make_user('alice') + self.bob = make_user('bob') + self.client.force_login(self.alice) + + def test_list_shows_own_items(self): + make_item(self.alice, name='Alice Game') + make_item(self.bob, name='Bob Game') + response = self.client.get(reverse('backlogger:list')) + self.assertContains(response, 'Alice Game') + self.assertNotContains(response, 'Bob Game') + + def test_list_category_filter(self): + make_item(self.alice, name='My Game', category=Item.GAMES) + make_item(self.alice, name='My Book', category=Item.BOOKS) + response = self.client.get(reverse('backlogger:list') + '?category=games') + self.assertContains(response, 'My Game') + self.assertNotContains(response, 'My Book') + + def test_list_empty_state(self): + response = self.client.get(reverse('backlogger:list')) + self.assertContains(response, 'Add your first one') + + def test_list_sort_az(self): + make_item(self.alice, name='Zelda') + make_item(self.alice, name='Ape') + response = self.client.get(reverse('backlogger:list') + '?sort=az') + names = [item.name for item in response.context['items']] + self.assertEqual(names, sorted(names)) + + def test_list_sort_favorites_first(self): + make_item(self.alice, name='Normal') + make_item(self.alice, name='Fav', favorite=True) + response = self.client.get(reverse('backlogger:list') + '?sort=fav') + names = [item.name for item in response.context['items']] + self.assertEqual(names[0], 'Fav') + + +# --------------------------------------------------------------------------- +# Item add +# --------------------------------------------------------------------------- + +class ItemAddTests(TestCase): + def setUp(self): + self.user = make_user('alice') + self.client.force_login(self.user) + + def test_add_game(self): + self.client.post(reverse('backlogger:add'), { + 'category': 'games', + 'name': 'Elden Ring', + 'progress_percent': 50, + 'favorite': False, + 'hours_played': 20, + 'total_hours': 80, + }) + item = Item.objects.get(name='Elden Ring') + self.assertEqual(item.user, self.user) + self.assertEqual(item.category, Item.GAMES) + self.assertEqual(item.hours_played, 20) + + def test_add_assigns_current_user(self): + self.client.post(reverse('backlogger:add'), { + 'category': 'books', + 'name': 'Dune', + 'progress_percent': 0, + 'favorite': False, + }) + item = Item.objects.get(name='Dune') + self.assertEqual(item.user, self.user) + + def test_add_invalid_shows_form(self): + response = self.client.post(reverse('backlogger:add'), {'name': ''}) + self.assertEqual(response.status_code, 200) + self.assertFalse(Item.objects.exists()) + + +# --------------------------------------------------------------------------- +# Item edit +# --------------------------------------------------------------------------- + +class ItemEditTests(TestCase): + def setUp(self): + self.alice = make_user('alice') + self.bob = make_user('bob') + self.client.force_login(self.alice) + + def test_edit_own_item(self): + item = make_item(self.alice, name='Original') + self.client.post(reverse('backlogger:edit', args=[item.pk]), { + 'category': 'games', + 'name': 'Updated', + 'progress_percent': 75, + 'favorite': False, + }) + item.refresh_from_db() + self.assertEqual(item.name, 'Updated') + + def test_edit_other_user_item_returns_404(self): + item = make_item(self.bob, name='Bobs Item') + response = self.client.get(reverse('backlogger:edit', args=[item.pk])) + self.assertEqual(response.status_code, 404) + + +# --------------------------------------------------------------------------- +# Item delete +# --------------------------------------------------------------------------- + +class ItemDeleteTests(TestCase): + def setUp(self): + self.alice = make_user('alice') + self.bob = make_user('bob') + self.client.force_login(self.alice) + + def test_delete_own_item(self): + item = make_item(self.alice, name='To Delete') + self.client.post(reverse('backlogger:delete', args=[item.pk])) + self.assertFalse(Item.objects.filter(pk=item.pk).exists()) + + def test_delete_other_user_item_returns_404(self): + item = make_item(self.bob, name='Bobs Item') + response = self.client.post(reverse('backlogger:delete', args=[item.pk])) + self.assertEqual(response.status_code, 404) + self.assertTrue(Item.objects.filter(pk=item.pk).exists()) + + +# --------------------------------------------------------------------------- +# Forms +# --------------------------------------------------------------------------- + +class ItemFormTests(TestCase): + def _post(self, **kwargs): + data = {'category': 'games', 'name': 'Test', 'progress_percent': 0, 'favorite': False} + data.update(kwargs) + form = ItemForm(data) + form.is_valid() + return form + + def test_game_fields_kept_for_games(self): + form = self._post(category='games', hours_played=10, total_hours=40) + self.assertEqual(form.cleaned_data['hours_played'], 10) + + def test_game_fields_nulled_for_books(self): + form = self._post(category='books', hours_played=10) + self.assertIsNone(form.cleaned_data['hours_played']) + + def test_book_fields_kept_for_books(self): + form = self._post(category='books', pages_read=100, total_pages=300) + self.assertEqual(form.cleaned_data['pages_read'], 100) + + def test_book_fields_nulled_for_games(self): + form = self._post(category='games', pages_read=100) + self.assertIsNone(form.cleaned_data['pages_read']) + + def test_film_fields_nulled_for_games(self): + form = self._post(category='games', duration_minutes=120) + self.assertIsNone(form.cleaned_data['duration_minutes']) + + def test_progress_over_100_invalid(self): + form = self._post(progress_percent=150) + self.assertIn('progress_percent', form.errors) + + def test_progress_negative_invalid(self): + form = self._post(progress_percent=-1) + self.assertIn('progress_percent', form.errors) + + +class SignupFormTests(TestCase): + def _data(self, **overrides): + data = { + 'username': 'newuser', + 'email': 'new@example.com', + 'password1': 'StrongPass99!', + 'password2': 'StrongPass99!', + } + data.update(overrides) + return data + + def test_valid_form(self): + form = SignupForm(self._data()) + self.assertTrue(form.is_valid()) + + def test_email_required(self): + form = SignupForm(self._data(email='')) + self.assertFalse(form.is_valid()) + self.assertIn('email', form.errors) + + def test_password_mismatch(self): + form = SignupForm(self._data(password2='different')) + self.assertFalse(form.is_valid())