diff --git a/src/main/java/com/itsthevine/web/AdminController.java b/src/main/java/com/itsthevine/web/AdminController.java index b0a2513..95494f6 100644 --- a/src/main/java/com/itsthevine/web/AdminController.java +++ b/src/main/java/com/itsthevine/web/AdminController.java @@ -58,14 +58,51 @@ public class AdminController { }); } + /** + * Everything you can do to one item, from one form. + * + *
One form per item rather than one per button: the page carries forty items, and a separate form + * for each control meant 467 forms and 464 CSRF tokens — 317 KB of HTML for a screen that is opened + * on a phone, in a bakery. {@code name="do"} says which button was pressed and its value carries the + * argument, exactly as the catering table editor does. + * + *
Only {@code save} looks at the name and category boxes. The other actions deliberately ignore
+ * them, so pressing "move down" halfway through retyping a name doesn't save the half-typed name.
+ *
+ * @param action {@code save}, {@code move:-1}, {@code move:1}, {@code delete}, or
+ * {@code photo:
@@ -14,34 +24,26 @@
Categories
-
On the page
diff --git a/src/test/java/com/itsthevine/web/AdminPagesTest.java b/src/test/java/com/itsthevine/web/AdminPagesTest.java
index e29ea89..066072c 100644
--- a/src/test/java/com/itsthevine/web/AdminPagesTest.java
+++ b/src/test/java/com/itsthevine/web/AdminPagesTest.java
@@ -94,6 +94,7 @@ class AdminPagesTest {
@Test
void renamingAnItemLandsAndSaysSo() throws Exception {
mvc.perform(post("/admin/items/1").with(user("morissa")).with(csrf())
+ .param("do", "save")
.param("name", "76th Birthday Cake (chocolate)")
.param("category", "Cakes"))
.andExpect(status().is3xxRedirection())
@@ -107,7 +108,7 @@ class AdminPagesTest {
@Test
void aRefusalComesBackAsASentenceTheEditorCanActOn() throws Exception {
// Cookies has items filed under it, and deleting the button shouldn't decide what happens to them.
- mvc.perform(post("/admin/categories/1/delete").with(user("morissa")).with(csrf()))
+ mvc.perform(post("/admin/categories/1").with(user("morissa")).with(csrf()).param("do", "delete"))
.andExpect(redirectedUrl("/admin"))
.andExpect(flash().attribute("problem", containsString("still filed under Cookies")));
}
@@ -115,7 +116,7 @@ class AdminPagesTest {
@Test
void movingAnItemUpFromTheTopIsNotAnError() throws Exception {
// The button is disabled in the page, but a stale page could still post this.
- mvc.perform(post("/admin/items/1/move").with(user("morissa")).with(csrf()).param("by", "-1"))
+ mvc.perform(post("/admin/items/1").with(user("morissa")).with(csrf()).param("do", "move:-1"))
.andExpect(redirectedUrl("/admin"))
.andExpect(flash().attributeCount(0));
}
diff --git a/src/test/java/com/itsthevine/web/AdminSecurityTest.java b/src/test/java/com/itsthevine/web/AdminSecurityTest.java
index 843bb25..c99fcba 100644
--- a/src/test/java/com/itsthevine/web/AdminSecurityTest.java
+++ b/src/test/java/com/itsthevine/web/AdminSecurityTest.java
@@ -3,7 +3,6 @@ package com.itsthevine.web;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.BeforeEach;
@@ -78,7 +77,8 @@ class AdminSecurityTest {
mvc.perform(get("/admin/catering/tables/1")).andExpect(status().isUnauthorized());
mvc.perform(post("/admin/items").with(csrf()).param("name", "Free cake").param("category", "Cakes"))
.andExpect(status().isUnauthorized());
- mvc.perform(post("/admin/items/1/delete").with(csrf())).andExpect(status().isUnauthorized());
+ mvc.perform(post("/admin/items/1").with(csrf()).param("do", "delete"))
+ .andExpect(status().isUnauthorized());
mvc.perform(post("/admin/categories").with(csrf()).param("name", "x"))
.andExpect(status().isUnauthorized());
// The prices are the one thing on this site a stranger would most enjoy editing.
@@ -113,8 +113,9 @@ class AdminSecurityTest {
@Test
void theContactFormStillNeedsItsCsrfToken() throws Exception {
- // Enabling the security starter enables CSRF for the PUBLIC contact form too. Without the token
- // it 403s; the SPA reads the XSRF-TOKEN cookie and sends X-XSRF-TOKEN.
+ // Enabling the security starter enables CSRF for the PUBLIC contact form too. Without a token it
+ // 403s. The page's own form carries a hidden field (Spring Security fills it in for any th:action
+ // form); this JSON endpoint needs the X-XSRF-TOKEN header from the cookie.
mvc.perform(post("/api/contact").contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Ada\",\"email\":\"ada@example.com\",\"message\":\"hi\"}"))
.andExpect(status().isForbidden());