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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
073ab85f30
commit
6bad2a2ad1
+33
-8
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user