diff --git a/src/test/java/catering/inventory/InventoryControllerIntegrationTests.java b/src/test/java/catering/inventory/InventoryControllerIntegrationTests.java
new file mode 100644
index 0000000..8e702c5
--- /dev/null
+++ b/src/test/java/catering/inventory/InventoryControllerIntegrationTests.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2023 Simon Bruder
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package catering.inventory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.endsWith;
+import static org.salespointframework.core.Currencies.EURO;
+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.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.javamoney.moneta.Money;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.salespointframework.catalog.Product.ProductIdentifier;
+import org.salespointframework.inventory.UniqueInventory;
+import org.salespointframework.inventory.UniqueInventoryItem;
+import org.salespointframework.quantity.Quantity;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+
+import catering.catalog.CatalogDummy;
+import catering.catalog.CatalogDummyType;
+
+@AutoConfigureMockMvc
+@SpringBootTest
+class InventoryControllerIntegrationTests {
+ @Autowired
+ MockMvc mvc;
+
+ @Autowired
+ UniqueInventory inventory;
+
+ UniqueInventoryItem anyInventoryItem;
+ CatalogDummy anyProduct;
+ ProductIdentifier anyPid;
+
+ @BeforeEach
+ void populateAnyInventoryItem() {
+ anyInventoryItem = inventory.findAll().stream().findAny().get();
+ anyProduct = (CatalogDummy) anyInventoryItem.getProduct();
+ anyPid = anyProduct.getId();
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = "ADMIN")
+ void adminCanList() throws Exception {
+ mvc.perform(get("/inventory"))
+ .andExpect(status().isOk())
+ .andExpect(content().string(containsString(anyPid.toString())));
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = "ADMIN")
+ void adminCanAdd() throws Exception {
+ mvc.perform(get("/inventory/add"))
+ .andExpect(status().isOk())
+ .andExpect(content().string(containsString("Produkt anlegen")));
+
+ long itemCountBefore = inventory.findAll().stream().count();
+
+ mvc.perform(post("/inventory/add")
+ .param("type", "CONSUMABLE")
+ .param("name", "MOCK Schnitzel Wiener Art (vegan)")
+ .param("quantity", "100")
+ .param("wholesalePrice", "3.00")
+ .param("retailPrice", "7.50")
+ .param("promotionPrice", "6.66"))
+ .andExpect(redirectedUrl("/inventory"));
+
+ long itemCountAfter = inventory.findAll().stream().count();
+
+ assertThat(itemCountAfter).isEqualTo(itemCountBefore + 1);
+
+ // TODO: this must be changed once the catalog split is done
+ assertThat(inventory.findAll().stream())
+ .extracting("product.type", "product.name", "quantity", "product.wholesalePrice", "product.price",
+ "product.promotionPrice")
+ .contains(tuple(CatalogDummyType.CONSUMABLE, "MOCK Schnitzel Wiener Art (vegan)", Quantity.of(100),
+ Money.of(3, EURO), Money.of(7.5, EURO), Money.of(6.66, EURO)));
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = "ADMIN")
+ void adminCanDelete() throws Exception {
+ long itemCountBefore = inventory.findAll().stream().count();
+
+ mvc.perform(get("/inventory/delete/" + anyPid))
+ .andExpect(redirectedUrl("/inventory"));
+
+ long itemCountAfter = inventory.findAll().stream().count();
+
+ assertThat(itemCountAfter).isEqualTo(itemCountBefore - 1);
+
+ // TODO: this must be changed once the catalog split is done
+ assertThat(inventory.findAll().stream())
+ .extracting("product.type", "product.name", "quantity")
+ .doesNotContain(tuple(anyProduct.getType(), anyProduct.getName(), anyInventoryItem.getQuantity()));
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = "ADMIN")
+ void adminCanEditConsumable() throws Exception {
+ // TODO: this must be changed once the catalog split is done
+ CatalogDummy anyConsumable = inventory.findAll().stream()
+ .map(UniqueInventoryItem::getProduct)
+ .map(p -> (CatalogDummy) p)
+ .filter(cd -> cd.getType().equals(CatalogDummyType.CONSUMABLE))
+ .findAny()
+ .get();
+
+ mvc.perform(get("/inventory/edit/" + anyConsumable.getId()))
+ .andExpect(status().isOk())
+ .andExpect(content().string(containsString("Produkt bearbeiten")));
+
+ boolean hasPromotionPrice = anyConsumable.getPromotionPrice() != null;
+
+ mvc.perform(post("/inventory/edit/" + anyConsumable.getId())
+ .param("type", "CONSUMABLE")
+ .param("name", "MOCK edited")
+ .param("quantity", "4711")
+ .param("wholesalePrice", "0.01")
+ .param("retailPrice", "0.01")
+ .param("promotionPrice", hasPromotionPrice ? "" : "5"))
+ .andExpect(redirectedUrl("/inventory"));
+
+ UniqueInventoryItem editedInventoryItem = inventory.findByProductIdentifier(anyConsumable.getId()).stream()
+ .findAny().get();
+ CatalogDummy editedProduct = (CatalogDummy) editedInventoryItem.getProduct();
+
+ assertThat(editedInventoryItem.getQuantity()).isEqualTo(Quantity.of(4711));
+ assertThat(editedProduct)
+ .extracting("type", "name", "wholesalePrice", "price", "promotionPrice")
+ .containsExactly(CatalogDummyType.CONSUMABLE, "MOCK edited", Money.of(0.01, EURO),
+ Money.of(0.01, EURO), hasPromotionPrice ? null : Money.of(0.01, EURO));
+ }
+
+ /**
+ * Helper function for asserting that protected endpoints are not accessible by
+ * unauthentificated users.
+ *
+ * @param resultActions the result of
+ * {@link org.springframework.test.web.servlet.MockMvc#perform}
+ * @return the {@link ResultActions} given as a parameter to perform more
+ * matching on
+ */
+ ResultActions assertRedirectsToLogin(ResultActions resultActions) throws Exception {
+ // Spring security uses the full URL to redirect to login,
+ // so we can’t use redirectedUrl("/login") in this case.
+ return resultActions
+ .andExpect(status().is3xxRedirection()) // concrete redirection type is implementation detail
+ .andExpect(header().string(HttpHeaders.LOCATION, endsWith("/login")));
+ }
+
+ @Test
+ void disallowUnauthorizedList() throws Exception {
+ assertRedirectsToLogin(mvc.perform(get("/inventory")));
+ }
+
+ @Test
+ void disallowUnauthorizedEditPage() throws Exception {
+ assertRedirectsToLogin(mvc.perform(get("/inventory/edit/" + anyPid)));
+ }
+
+ @Test
+ void disallowUnauthorizedEdit() throws Exception {
+ assertRedirectsToLogin(mvc.perform(post("/inventory/edit/" + anyPid)));
+ }
+
+ @Test
+ void disallowUnauthorizedAddPage() throws Exception {
+ assertRedirectsToLogin(mvc.perform(get("/inventory/add")));
+ }
+
+ @Test
+ void disallowUnauthorizedAdd() throws Exception {
+ assertRedirectsToLogin(mvc.perform(post("/inventory/add")));
+ }
+
+ @Test
+ void disallowUnauthorizedDelete() throws Exception {
+ assertRedirectsToLogin(mvc.perform(get("/inventory/delete/" + anyPid)));
+ }
+}