From d92deeab514a52e7c3416baef78f8969ade951cc Mon Sep 17 00:00:00 2001 From: Tom Flux Date: Tue, 23 Jun 2026 20:37:48 +0100 Subject: 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 --- kitchen/tests.py | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) (limited to 'kitchen/tests.py') 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): -- cgit v1.2.3