diff options
| author | Tom Flux <tom@tomflux.xyz> | 2026-06-23 20:37:48 +0100 |
|---|---|---|
| committer | Tom Flux <tom@tomflux.xyz> | 2026-06-23 20:37:48 +0100 |
| commit | d92deeab514a52e7c3416baef78f8969ade951cc (patch) | |
| tree | f5c071147ea6f62501dee904a1f381805a1e5941 /kitchen/tests.py | |
| parent | 682c0f76b85950ad0889e11750dc84e0f00d2807 (diff) | |
Phase 2: session auth for the web UI
Require login for /app/, with a long sliding session so you log in
once per device and effectively stay in.
- AppLoginRequiredMiddleware gates only /app/; /api/ keeps DRF token
auth and /admin/ keeps its own login (a blanket LoginRequired would
break token requests, whose user isn't resolved until the view runs).
- Login page (styled to the dark palette) via django.contrib.auth.urls;
logout control in the nav.
- Session: ~1 year cookie, sliding (saved every request), survives
browser close.
- Dropped every @csrf_exempt now that a real session + CSRF token are
in place (HTMX already sends X-CSRFToken).
- SECRET_KEY and DEBUG now read from the environment (prod-safe
defaults); systemd loads an optional /var/lib/food/.env.
- Tests authenticate, plus new coverage: /app/ redirects when logged
out, login grants access, /api/ is not caught by the app gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'kitchen/tests.py')
| -rw-r--r-- | kitchen/tests.py | 41 |
1 files changed, 33 insertions, 8 deletions
diff --git a/kitchen/tests.py b/kitchen/tests.py index 31e0665..065d530 100644 --- a/kitchen/tests.py +++ b/kitchen/tests.py @@ -1,12 +1,39 @@ from django.test import TestCase, Client from django.urls import reverse +from django.contrib.auth.models import User from kitchen.models import Ingredient, PantryItem, MetaRecipe, Slot, SlotOption -class PantryStateTests(TestCase): +class AuthTests(TestCase): + def test_app_requires_login(self): + resp = self.client.get(reverse("app-pantry")) + self.assertEqual(resp.status_code, 302) + self.assertIn(reverse("login"), resp.url) + + def test_login_grants_access(self): + User.objects.create_user("tom", password="pw") + self.assertTrue(self.client.login(username="tom", password="pw")) + resp = self.client.get(reverse("app-pantry")) + self.assertEqual(resp.status_code, 200) + + def test_api_is_not_redirected_to_login(self): + # /api/ uses DRF token auth; the /app/ login middleware must not touch it. + resp = self.client.get("/api/pantry/") + self.assertIn(resp.status_code, (401, 403)) + self.assertNotEqual(resp.status_code, 302) + + +class _AuthedTestCase(TestCase): def setUp(self): self.client = Client() + self.user = User.objects.create_user("tester", password="pw") + self.client.force_login(self.user) + + +class PantryStateTests(_AuthedTestCase): + def setUp(self): + super().setUp() self.eggs = Ingredient.objects.create(name="eggs", default_unit="items") self.item = PantryItem.objects.create( ingredient=self.eggs, location="fridge", state="in" @@ -56,7 +83,6 @@ class PantryStateTests(TestCase): reverse("app-pantry-add"), {"ingredient_name": "eggs", "location": "fridge"}, ) - # No duplicate row, and the existing one is back in stock. items = PantryItem.objects.filter(ingredient=self.eggs, location="fridge") self.assertEqual(items.count(), 1) self.assertEqual(items.first().state, "in") @@ -68,12 +94,12 @@ class PantryStateTests(TestCase): self.assertContains(resp, "eggs") -class RecipesPresenceTests(TestCase): - """The recipes matcher is presence-based now: having an ingredient (not - 'out') satisfies a slot regardless of quantity.""" +class RecipesPresenceTests(_AuthedTestCase): + """Presence-based matcher: having an ingredient (not 'out') satisfies a slot + regardless of quantity.""" def setUp(self): - self.client = Client() + super().setUp() self.noodles = Ingredient.objects.create(name="noodles", default_unit="nests") self.mr = MetaRecipe.objects.create(name="Noodles", method="boil") slot = Slot.objects.create(meta_recipe=self.mr, name="carb", required=True) @@ -82,7 +108,6 @@ class RecipesPresenceTests(TestCase): ) def test_present_ingredient_no_quantity_is_available(self): - # 'in' with no quantity should still count as available. PantryItem.objects.create( ingredient=self.noodles, location="cupboard", state="in" ) @@ -98,7 +123,7 @@ class RecipesPresenceTests(TestCase): self.assertEqual(resp.status_code, 200) -class PageSmokeTests(TestCase): +class PageSmokeTests(_AuthedTestCase): def test_pages_render(self): for name in ("app-pantry", "app-recipes", "app-shopping", "app-log"): with self.subTest(page=name): |
