From 07d3fe304dcae24811ec15e49f07eefe9a656c14 Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Thu, 27 Nov 2025 14:29:18 +0100 Subject: [PATCH 01/15] Revert "Merge branch 'backend' into 'main'" This reverts merge request !2 --- commons/src/main/java/commons/Recipe.java | 216 ---------------- commons/src/test/java/commons/RecipeTest.java | 121 --------- docs/recipe-api.md | 243 ------------------ .../java/server/api/RecipeController.java | 130 ---------- .../server/database/RecipeRepository.java | 22 -- .../application-mock-data-test.properties | 18 -- .../java/server/api/RecipeControllerTest.java | 149 ----------- 7 files changed, 899 deletions(-) delete mode 100644 commons/src/main/java/commons/Recipe.java delete mode 100644 commons/src/test/java/commons/RecipeTest.java delete mode 100644 docs/recipe-api.md delete mode 100644 server/src/main/java/server/api/RecipeController.java delete mode 100644 server/src/main/java/server/database/RecipeRepository.java delete mode 100644 server/src/main/resources/application-mock-data-test.properties delete mode 100644 server/src/test/java/server/api/RecipeControllerTest.java diff --git a/commons/src/main/java/commons/Recipe.java b/commons/src/main/java/commons/Recipe.java deleted file mode 100644 index 0472175..0000000 --- a/commons/src/main/java/commons/Recipe.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright 2021 Delft University of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package commons; - -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import jakarta.persistence.Entity; -import jakarta.persistence.CollectionTable; -import jakarta.persistence.Column; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OrderColumn; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.ElementCollection; - - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Collection; - -// TABLE named recipes -@Entity -@Table(name = "recipes") -public class Recipe { - - // PRIMARY Key, unique id for recipe, generated automatically. - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - @Column(name = "id", nullable = false, unique = true) - private Long id; - - // Recipe name - @Column(name = "name", nullable = false, unique = true) - private String name; - - /** - * Creates another table named recipe_ingredients which stores: - * recipe_ingredients(recipe_id -> recipes(id), ingredient). - *

- * Example recipe_ingredients table: - *

-     * | recipe_id           | ingredient |
-     * |---------------------|------------|
-     * | 0 (Chocolate Cake)  | Egg        |
-     * | 0 (Chocolate Cake)  | Egg        |
-     * | 0 (Chocolate Cake)  | Flour      |
-     * | 1 (Steak)           | 40g salt   |
-     * | 1 (Steak)           | 40g pepper |
-     * | 1 (Steak)           | Meat       |
-     * |----------------------------------|
-     * 
- * TODO: Replace String with Embeddable Ingredient Class - */ - @ElementCollection - @CollectionTable(name = "recipe_ingredients", joinColumns = @JoinColumn(name = "recipe_id")) - @Column(name = "ingredient") - private List ingredients = new ArrayList<>(); - - /** - * Creates another table named recipe_preparation which stores: - * recipe_preparation(recipe_id -> recipes(id), preparation_step, step_order). - *

- * Example recipe_preparation table: - *

-     * | recipe_id           | preparation_step     | step_order |
-     * |---------------------|----------------------|------------|
-     * | 0 (Chocolate Cake)  | Preheat oven         | 1          |
-     * | 0 (Chocolate Cake)  | Mix eggs and sugar   | 2          |
-     * | 0 (Chocolate Cake)  | Add flour gradually  | 3          |
-     * | 1 (Steak)           | Season meat          | 1          |
-     * | 1 (Steak)           | Heat pan             | 2          |
-     * |---------------------------------------------------------|
-     * 
- */ - @ElementCollection - @CollectionTable(name = "recipe_preparation", joinColumns = @JoinColumn(name = "recipe_id")) - @Column(name = "preparation_step") - @OrderColumn(name = "step_order") - private List preparationSteps = new ArrayList<>(); - - @SuppressWarnings("unused") - public Recipe() { - // for object mapper - } - - public Recipe(Long id, String name) { - // Not used by JPA/Spring - this.id = id; - this.name = name; - } - - // TODO: Replace String with Embeddable Ingredient Class for ingredients - public Recipe(Long id, String name, List ingredients, List preparationSteps) { - // Not used by JPA/Spring - this.id = id; - this.name = name; - this.ingredients = ingredients; - this.preparationSteps = preparationSteps; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - /** - * Returns an unmodifiable view of the ingredients list. - *

- * The returned list cannot be modified directly. To modify ingredients, - * create a copy using {@link List#copyOf(Collection)} or create your own list, - * populate it, and use {@link #setIngredients(List)} to update. - * - * @return An unmodifiable list of ingredients. - * @see #setIngredients(List) - */ - // TODO: Replace String with Embeddable Ingredient Class - public List getIngredients() { - // Disallow modifying the returned list. - // You can still copy it with List.copyOf(...) - return Collections.unmodifiableList(ingredients); - } - - - // TODO: Replace String with Embeddable Ingredient Class - public void setIngredients(List ingredients) { - this.ingredients = ingredients; - } - - /** - * Returns an unmodifiable view of the preparation steps list. - *

- * The returned list cannot be modified directly. To modify preparation steps, - * create a copy using {@link List#copyOf(Collection)} or create your own list, - * populate it, and use {@link #setPreparationSteps(List)} to update. - * - * @return An unmodifiable list of preparation steps in order. - * @see #setPreparationSteps(List) - */ - public List getPreparationSteps() { - // Disallow modifying the returned list. - // You can still copy it with List.copyOf(...) - return Collections.unmodifiableList(preparationSteps); - } - - public void setPreparationSteps(List preparationSteps) { - this.preparationSteps = preparationSteps; - } - - @Override - public boolean equals(Object o) { - // Faster/Better equals than reflectionEquals - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Recipe recipe = (Recipe) o; - return Objects.equals(id, recipe.id); // Check only for ID, as its unique! - } - - @Override - public int hashCode() { - return Objects.hash(id); // Use ID only, as its unique. - } - - @Override - public String toString() { - return "Recipe{" + - "name='" + name + '\'' + - ", ingredientsCount=" + ingredients.size() + - ", preparationStepsCount=" + preparationSteps.size() + - "}"; - } - - /** - * Returns a more detailed string than {@link #toString()} of this recipe, including - * the full contents of all ingredients and preparation steps. - *

- * Intended only for debugging. - * - * @return A detailed string representation containing all recipe data. - * @see #toString() - */ - @SuppressWarnings("unused") - public String toDetailedString() { - return "Recipe{" + - "name='" + name + '\'' + - ", ingredients=" + ingredients + - ", preparationSteps=" + preparationSteps + - '}'; - } - -} diff --git a/commons/src/test/java/commons/RecipeTest.java b/commons/src/test/java/commons/RecipeTest.java deleted file mode 100644 index db70bf2..0000000 --- a/commons/src/test/java/commons/RecipeTest.java +++ /dev/null @@ -1,121 +0,0 @@ -package commons; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -class RecipeTest { - - Recipe recipe; - static final Long RECIPE_ID = 1L; - - @BeforeEach - void setupRecipe() { - this.recipe = new Recipe(RECIPE_ID, "Chocolate Cake"); - } - - @Test - void getId() { - assertEquals(RECIPE_ID, recipe.getId()); - } - - @Test - void setId() { - recipe.setId(RECIPE_ID + 1); - assertEquals(RECIPE_ID + 1, recipe.getId()); - } - - @Test - void getName() { - assertEquals("Chocolate Cake", recipe.getName()); - } - - @Test - void setName() { - recipe.setName("Steak"); - assertEquals("Steak", recipe.getName()); - } - - @Test - void getIngredientsAddThrow() { - // TODO: Change to actual Ingredient class later - assertThrows(UnsupportedOperationException.class, () -> recipe.getIngredients().add("Lasagna")); - } - - @Test - void getIngredientsClearThrow() { - assertThrows(UnsupportedOperationException.class, () -> recipe.getIngredients().clear()); - } - - @Test - void setIngredients() { - // TODO: Change to actual Ingredient class later - List ingredients = new ArrayList<>(List.of("Chocolate", "Flour", "Egg")); - recipe.setIngredients(ingredients); - - assertEquals(recipe.getIngredients(), ingredients); - assertEquals(recipe.getIngredients().size(), ingredients.size()); - } - - @Test - void getPreparationStepsAddThrow() { - assertThrows(UnsupportedOperationException.class, () -> recipe.getPreparationSteps().add("Preheat Oven")); - } - - @Test - void getPreparationStepsClearThrow() { - assertThrows(UnsupportedOperationException.class, () -> recipe.getPreparationSteps().clear()); - } - - @Test - void setPreparationSteps() { - List steps = new ArrayList<>(List.of("Preheat oven", "Mix stuff", "decorate")); - recipe.setPreparationSteps(steps); - - assertEquals(recipe.getPreparationSteps(), steps); - assertEquals(recipe.getPreparationSteps().size(), steps.size()); - } - - @Test - void testEqualsSame() { - Recipe recipe2 = new Recipe(RECIPE_ID, "Chocolate Cake"); - assertEquals(recipe, recipe2); - } - - @Test - void testEqualsSameExceptId() { - Recipe recipe2 = new Recipe(RECIPE_ID + 1, "Chocolate Cake"); - assertNotEquals(recipe, recipe2); - } - - @Test - void testEqualsOnlySameId() { - Recipe recipe2 = new Recipe(RECIPE_ID, "Some random recipe"); - assertEquals(recipe, recipe2); // Equals, we only look at ID!!! - } - - @Test - void testHashCodeSame() { - Recipe recipe2 = new Recipe(RECIPE_ID, "Chocolate Cake"); - - assertEquals(recipe.hashCode(), recipe2.hashCode()); - } - - @Test - void testHashCodeSameExceptId() { - Recipe recipe2 = new Recipe(RECIPE_ID + 1, "Chocolate Cake"); - - assertNotEquals(recipe.hashCode(), recipe2.hashCode()); - } - - @Test - void testHashCodeOnlySameId() { - Recipe recipe2 = new Recipe(RECIPE_ID, "Some random recipe"); - - assertEquals(recipe.hashCode(), recipe2.hashCode()); // Same, only looks at ID!!! - } -} \ No newline at end of file diff --git a/docs/recipe-api.md b/docs/recipe-api.md deleted file mode 100644 index e2b9b4f..0000000 --- a/docs/recipe-api.md +++ /dev/null @@ -1,243 +0,0 @@ -## Recipe API - -Base path: `/api` - -This API allows clients to create, read, update, and delete `Recipe` resources. - ---- - -## Endpoints - -### Get a Recipe by ID - -**GET** `/api/recipe/{id}` - -Retrieves a specific recipe by its unique identifier. - -* **Path Parameters** - - | Name | Type | Required | Description | - | ---- | ------ | -------- | ------------------------------ | - | `id` | `Long` | Yes | The ID of the recipe to fetch. | - -* **Responses** - - | Status Code | Description | - | --------------- | ------------------------------------- | - | `200 OK` | Returns the recipe with the given ID. | - | `404 Not Found` | No recipe exists with that ID. | - -* **Example Request** - - ```bash - curl -X GET "http://SERVER_ADDRESS/api/recipe/1" \ - -H "Accept: application/json" - ``` - -* **Example Response (200)** - - ```json - { - "id": 1, - "name": "Pancakes", - "ingredients": ["Ingredient 1", "Ingredient 2"], - "preparationSteps": ["Step 1", "Step 2"] - } - ``` - ---- - -### List Recipes - -**GET** `/api/recipes` - -Retrieves a list of recipes. Supports optional pagination via a `limit` query parameter. - -* **Query Parameters** - - | Name | Type | Required | Description | - | ------- | --------- | -------- | ---------------------------------------------------------------------------- | - | `limit` | `Integer` | No | Maximum number of recipes to return. If not provided, returns *all* recipes. | - -* **Responses** - - | Status Code | Description | - | ----------- | --------------------------------------------- | - | `200 OK` | Returns a list of recipes (possibly limited). | - -* **Example Request (no limit)** - - ```bash - curl -X GET "http://SERVER_ADDRESS/api/recipes" \ - -H "Accept: application/json" - ``` - -* **Example Request (with limit)** - - ```bash - curl -X GET "http://SERVER_ADDRESS/api/recipes?limit=10" \ - -H "Accept: application/json" - ``` - -* **Example Response (200)** - - ```json - [ - { - "id": 10, - "name": "Recipe 0", - "ingredients": ["Flour", "Milk"], - "preparationSteps": ["Do something", "Do something else"] - }, - { - "id": 11, - "name": "Recipe 1", - "ingredients": ["Flour", "Milk"], - "preparationSteps": ["Do something", "Do something else"] - } - // etc. max {limit} items - ] - ``` - ---- - -### Create a New Recipe - -**PUT** `/api/recipe/new` - -Creates a new recipe. The recipe name must be unique in the repository. - -* **Request Body** - - A JSON object representing the recipe. Example: - - ```json - { - "name": "Pancakes", - "ingredients": ["Flour", "Milk", "Eggs"], - "preparationSteps": ["Step 1", "Step 2"] - } - ``` - -* **Responses** - - | Status Code | Description | - | ----------------- | -------------------------------------------------------------------------------------------- | - | `200 OK` | The recipe was successfully created. Returns the created recipe (including its assigned ID). | - | `400 Bad Request` | A recipe with the same name already exists. | - -* **Example Request** - - ```bash - curl -X PUT "https://SERVER_ADDRESS/api/recipe/new" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Pancakes", - "ingredients": ["Flour", "Milk", "Eggs"], - "preparationSteps": ["Step 1", "Step 2"] - }' - ``` - -* **Example Response (200)** - - ```json - { - "id": 125, - "name": "Pancakes", - "ingredients": ["Flour", "Milk", "Eggs"], - "preparationSteps": ["Step 1", "Step 2"] - } - ``` - ---- - -### Update an Existing Recipe - -**POST** `/api/recipe/{id}` - -Replaces or updates the recipe with the given ID. - -* **Path Parameters** - - | Name | Type | Required | Description | - | ---- | ------ | -------- | ------------------------------- | - | `id` | `Long` | Yes | The ID of the recipe to update. | - -* **Request Body** - - A JSON object containing the new recipe data. It is expected to include the ID (or the server-side save will override it), or at least map correctly to the stored entity. - - Example: - - ```json - { - "id": 123, - "name": "Better Pancakes", - "ingredients": ["Flour", "Almond milk", "Eggs"], - "preparationSteps": ["Step 10", "Step 11"] - } - ``` - -* **Responses** - - | Status Code | Description | - | ----------------- | ---------------------------------------------------------------- | - | `200 OK` | The recipe was successfully updated. Returns the updated recipe. | - | `400 Bad Request` | No recipe exists with the given ID. | - - * **Example Request** - - ```bash - curl -X POST "https://your-domain.com/api/recipe/123" \ - -H "Content-Type: application/json" \ - -d '{ - "id": 123, - "name": "Better Pancakes", - "ingredients": ["Flour", "Almond milk", "Eggs"], - "preparationSteps": ["Step 10", "Step 11"] - }' - ``` - -* **Example Response (200)** - - ```json - { - "id": 123, - "name": "Updated Pancakes", - "ingredients": ["Flour", "Almond milk", "Eggs"], - "preparationSteps": "Mix and fry differently." - } - ``` - ---- - -### Delete a Recipe - -**DELETE** `/api/recipe/{id}` - -Deletes the recipe with the given ID. - -* **Path Parameters** - - | Name | Type | Required | Description | - | ---- | ------ | -------- | ------------------------------- | - | `id` | `Long` | Yes | The ID of the recipe to delete. | - -* **Responses** - - | Status Code | Description | - | ----------------- | ---------------------------------------------------- | - | `200 OK` | The recipe was successfully deleted. Returns `true`. | - | `400 Bad Request` | No recipe exists with the given ID. | - -* **Example Request** - - ```bash - curl -X DELETE "https://your-domain.com/api/recipe/123" - ``` - -* **Example Response (200)** - - ```json - true - ``` \ No newline at end of file diff --git a/server/src/main/java/server/api/RecipeController.java b/server/src/main/java/server/api/RecipeController.java deleted file mode 100644 index a1d79fe..0000000 --- a/server/src/main/java/server/api/RecipeController.java +++ /dev/null @@ -1,130 +0,0 @@ -package server.api; - -import commons.Recipe; - -import org.springframework.data.domain.Example; -import org.springframework.data.domain.PageRequest; -import org.springframework.http.ResponseEntity; - -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import server.database.RecipeRepository; - -import java.util.List; -import java.util.Optional; - -@RestController -@RequestMapping("/api") -public class RecipeController { - private final RecipeRepository recipeRepository; // JPA repository used in this controller - - public RecipeController(RecipeRepository recipeRepository) { - this.recipeRepository = recipeRepository; - } - - /** - * Mapping for GET /recipe/{id} - *

- * Gets a specific recipe by its unique id. - *

- * @param id id of the recipe - * @return The recipe if it exists in the repository; otherwise returns 404 Not Found status - */ - @GetMapping("/recipe/{id}") - public ResponseEntity getRecipe(@PathVariable Long id) { - if (!recipeRepository.existsById(id)) { - return ResponseEntity.notFound().build(); - } - return ResponseEntity.ok(recipeRepository.findById(id).get()); - } - - /** - * Mapping for GET /recipes(?limit=) - *

- * If the limit parameter is unspecified, return all recipes in the repository. - * @param limit Integer limit of items you want to get - * @return The list of recipes - */ - @GetMapping("/recipes") - public ResponseEntity> getRecipes(@RequestParam Optional limit) { - if (limit.isPresent()) { - return ResponseEntity.ok( - recipeRepository.findAll( - PageRequest.of(0, limit.get()) - ).toList()); - } - return ResponseEntity.ok(recipeRepository.findAll()); - } - - /** - * Mapping for POST /recipe/{id} - * @param id The recipe id to replace - * @param recipe The new recipe to be replaced from the original - * @return The changed recipe; returns 400 Bad Request if the recipe does not exist - */ - @PostMapping("/recipe/{id}") - public ResponseEntity updateRecipe(@PathVariable Long id, @RequestBody Recipe recipe) { - if (!recipeRepository.existsById(id)) { - return ResponseEntity.badRequest().build(); - } - - // TODO: Send WS update to all subscribers with the updated recipe - - return ResponseEntity.ok(recipeRepository.save(recipe)); - } - - /** - * Mapping for PUT /recipe/new - *

- * Inserts a new recipe into the repository - *

- * @param recipe The new recipe as a request body - * @return 200 OK with the recipe you added; or 400 Bad Request if the recipe already exists by name - */ - @PutMapping("/recipe/new") - public ResponseEntity createRecipe(@RequestBody Recipe recipe) { - - // We initialize a new example recipe with the name of input recipe - // This is the only attribute we are concerned about making sure it's unique - Recipe example = new Recipe(); - example.setName(recipe.getName()); - - /* Here we use very funny JPA magic repository.exists(Example) - We check if any recipe in the repository has the same name as the input - */ - if (recipeRepository.exists(Example.of(example))) { - return ResponseEntity.badRequest().build(); - } - - // TODO: Send WS update to all subscribers with the new recipe - - return ResponseEntity.ok(recipeRepository.save(recipe)); - } - - /** - * Mapping for DELETE /recipe/{id} - *

- * Deletes a recipe identified by its id. - *

- * @param id The id of the recipe to be deleted. - * @return 200 OK with true; or 400 Bad Request if the recipe doesn't exist. - */ - @DeleteMapping("/recipe/{id}") - public ResponseEntity deleteRecipe(@PathVariable Long id) { - if (!recipeRepository.existsById(id)) { - return ResponseEntity.badRequest().build(); - } - recipeRepository.deleteById(id); - - // TODO: Send WS update to propagate deletion - return ResponseEntity.ok(true); - } -} diff --git a/server/src/main/java/server/database/RecipeRepository.java b/server/src/main/java/server/database/RecipeRepository.java deleted file mode 100644 index 60ec130..0000000 --- a/server/src/main/java/server/database/RecipeRepository.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2021 Delft University of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package server.database; - -import org.springframework.data.jpa.repository.JpaRepository; - -import commons.Recipe; - -public interface RecipeRepository extends JpaRepository {} \ No newline at end of file diff --git a/server/src/main/resources/application-mock-data-test.properties b/server/src/main/resources/application-mock-data-test.properties deleted file mode 100644 index c68d6fa..0000000 --- a/server/src/main/resources/application-mock-data-test.properties +++ /dev/null @@ -1,18 +0,0 @@ -spring.datasource.driverClassName=org.h2.Driver -spring.datasource.username=sa -spring.datasource.password= -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect - -# use one of these alternatives... -# ... purely in-memory, wiped on restart, but great for testing -spring.datasource.url=jdbc:h2:mem:testdb -# ... persisted on disk (in project directory) -#spring.datasource.url=jdbc:h2:file:./h2-database - -# enable DB view on http://localhost:8080/h2-console -spring.h2.console.enabled=true - -# strategy for table (re-)generation -spring.jpa.hibernate.ddl-auto=update -# show auto-generated SQL commands -#spring.jpa.hibernate.show_sql=true diff --git a/server/src/test/java/server/api/RecipeControllerTest.java b/server/src/test/java/server/api/RecipeControllerTest.java deleted file mode 100644 index f1a766e..0000000 --- a/server/src/test/java/server/api/RecipeControllerTest.java +++ /dev/null @@ -1,149 +0,0 @@ -package server.api; - - -import commons.Recipe; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInfo; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.http.HttpStatus; -import org.springframework.test.context.ActiveProfiles; -import server.database.RecipeRepository; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.LongStream; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -// Spring Boot unit testing magic -// Before each test the state of the repository is reset by -// rolling back transaction of changes from previous test -@DataJpaTest - -// This test sources its application profile from -// resources/application-mock-data-test.properties -// This config uses an in-memory database -@ActiveProfiles("mock-data-test") -public class RecipeControllerTest { - private RecipeController controller; - private List recipes; - private final RecipeRepository recipeRepository; - private List recipeIds; - public static final int NUM_RECIPES = 10; - - // Injects a test repository into the test class - @Autowired - public RecipeControllerTest(RecipeRepository recipeRepository) { - this.recipeRepository = recipeRepository; - } - - @BeforeEach - public void setup(TestInfo info) { - recipes = LongStream - .range(0, NUM_RECIPES) - .mapToObj(x -> new Recipe(null, "Recipe " + x, List.of(), List.of())) - .toList(); - controller = new RecipeController(recipeRepository); - Set tags = info.getTags(); - List ids = new ArrayList<>(); - - // Some tests want initial data to be created. - if (tags.contains("test-from-init-data")) { - ids = LongStream - .range(0, NUM_RECIPES) - .map(idx -> recipeRepository.save(recipes.get((int) idx)).getId()) - .boxed().toList(); - } - - // Some tests need to know the stored IDs of objects - if (tags.contains("need-ids")) { - recipeIds = ids; - } - - // If no tags specified, the repository is initialized as empty. - } - @Test - public void createOneRecipe() { - controller.createRecipe(recipes.getFirst()); - - // There is 1 recipe in the repository - assertEquals(1, recipeRepository.count()); - } - @Test - public void createManyRecipes() { - recipes.forEach(recipe -> controller.createRecipe(recipe)); - - // There are the same number of recipes in the repository as the input list - assertEquals(recipes.size(), recipeRepository.count()); - } - @Test - @Tag("test-from-init-data") - public void getManyRecipes() { - // The number of recipes returned is the same as the entire input list - assertEquals(recipes.size(), controller.getRecipes(Optional.empty()).getBody().size()); - } - @Test - @Tag("test-from-init-data") - public void getSomeRecipes() { - final int LIMIT = 5; - // The number of recipes returned is the same as the entire input list - assertEquals(LIMIT, controller.getRecipes(Optional.of(LIMIT)).getBody().size()); - } - @Test - @Tag("test-from-init-data") - @Tag("need-ids") - public void findOneRecipeExists() { - final int CHECK_INDEX = 3; - // The third item in the input list is the same as the third item retrieved from the database - assertEquals( - recipes.get(CHECK_INDEX), - controller.getRecipe(recipeIds.get(CHECK_INDEX)).getBody()); - } - @Test - public void findOneRecipeNotExists() { - final int CHECK_INDEX = 3; - // There does not exist a recipe with ID=3 since there are no items in the repository. - assertEquals( - HttpStatus.NOT_FOUND, - controller.getRecipe((long) CHECK_INDEX).getStatusCode()); - } - @Test - @Tag("test-from-init-data") - @Tag("need-ids") - public void deleteOneRecipeGood() { - final int DELETE_INDEX = 5; - - // The object has been successfully deleted - assertEquals(HttpStatus.OK, controller.deleteRecipe(recipeIds.get(DELETE_INDEX)).getStatusCode()); - } - @Test - @Tag("test-from-init-data") - @Tag("need-ids") - public void deleteOneRecipeCountGood() { - final int DELETE_INDEX = 5; - controller.deleteRecipe(recipeIds.get(DELETE_INDEX)); - // The count of items decreased by 1 after the 5th item has been removed. - assertEquals(recipeIds.size() - 1, recipeRepository.count()); - } - @Test - public void deleteOneRecipeFail() { - final Long DELETE_INDEX = 5L; - assertEquals(HttpStatus.BAD_REQUEST, controller.deleteRecipe(DELETE_INDEX).getStatusCode()); - } - - @Test - @Tag("test-from-init-data") - @Tag("need-ids") - public void updateOneRecipeHasNewData() { - final int UPDATE_INDEX = 5; - Recipe newRecipe = controller.getRecipe(recipeIds.get(UPDATE_INDEX)).getBody(); - newRecipe.setName("New recipe"); - controller.updateRecipe(newRecipe.getId(), newRecipe); - assertEquals("New recipe", recipeRepository.getReferenceById(recipeIds.get(UPDATE_INDEX)).getName()); - } -} From 646183174a3833db763d9a224ab0ea193ec47a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Rasie=C5=84ski?= Date: Fri, 21 Nov 2025 19:32:40 +0100 Subject: [PATCH 02/15] Created basic recipe. --- commons/src/main/java/commons/Recipe.java | 166 ++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 commons/src/main/java/commons/Recipe.java diff --git a/commons/src/main/java/commons/Recipe.java b/commons/src/main/java/commons/Recipe.java new file mode 100644 index 0000000..951fc85 --- /dev/null +++ b/commons/src/main/java/commons/Recipe.java @@ -0,0 +1,166 @@ +/* + * Copyright 2021 Delft University of Technology + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package commons; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +// TABLE named recipes +@Entity +@Table(name = "recipes") +public class Recipe { + + // PRIMARY Key, unique id for recipe, generated automatically. + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "id", nullable = false, unique = true) + private Long id; + + // Recipe name + @Column(name = "name", nullable = false, unique = true) + private String name; + + // Creates another table named recipe_ingredients which stores: + // recipe_ingredients(recipe_id -> recipes(id), ingredient). + // Example recipe_ingredients table: + // | recipe_id | ingredient | + // |---------------------|------------| + // | 0 (Chocolate Cake) | Egg | + // | 0 (Chocolate Cake) | Egg | + // | 0 (Chocolate Cake) | Flour | + // | 1 (Steak) | 40g salt | + // | 1 (Steak) | 40g pepper | + // | 1 (Steak) | Meat | + // |----------------------------------| + @ElementCollection + @CollectionTable(name = "recipe_ingredients", joinColumns = @JoinColumn(name = "recipe_id")) + @Column(name = "ingredient") + // TODO: Replace String with Embeddable Ingredient Class + private List ingredients = new ArrayList<>(); + + // Creates another table named recipe_preparation which stores: + // recipe_preparation(recipe_id -> recipes(id), preparation_step, step_order). + // Example recipe_preparation table: + // | recipe_id | preparation_step | step_order | + // |---------------------|----------------------|------------| + // | 0 (Chocolate Cake) | Preheat oven | 1 | + // | 0 (Chocolate Cake) | Mix eggs and sugar | 2 | + // | 0 (Chocolate Cake) | Add flour gradually | 3 | + // | 1 (Steak) | Season meat | 1 | + // | 1 (Steak) | Heat pan | 2 | + // |---------------------------------------------------------| + @ElementCollection + @CollectionTable(name = "recipe_preparation", joinColumns = @JoinColumn(name = "recipe_id")) + @Column(name = "preparation_step") + @OrderColumn(name = "step_order") + private List preparationSteps = new ArrayList<>(); + + @SuppressWarnings("unused") + private Recipe() { + // for object mapper + } + + public Recipe(Long id, String name) { + // Not used by JPA/Spring + this.id = id; + this.name = name; + } + + // TODO: Replace String with Embeddable Ingredient Class for ingredients + public Recipe(Long id, String name, List ingredients, List preparationSteps) { + // Not used by JPA/Spring + this.id = id; + this.name = name; + this.ingredients = ingredients; + this.preparationSteps = preparationSteps; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + // TODO: Replace String with Embeddable Ingredient Class + public List getIngredients() { + // Disallow modifying the returned list. + // You can still copy it with List.copyOf(...) + return Collections.unmodifiableList(ingredients); + } + + // TODO: Replace String with Embeddable Ingredient Class + public void setIngredients(List ingredients) { + this.ingredients = ingredients; + } + + public List getPreparationSteps() { + // Disallow modifying the returned list. + // You can still copy it with List.copyOf(...) + return Collections.unmodifiableList(preparationSteps); + } + + public void setPreparationSteps(List preparationSteps) { + this.preparationSteps = preparationSteps; + } + + @Override + public boolean equals(Object o) { + // Faster/Better equals than reflectionEquals + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Recipe recipe = (Recipe) o; + return Objects.equals(id, recipe.id); // Check only for ID, as its unique! + } + + @Override + public int hashCode() { + return Objects.hash(id); // Use ID only, as its unique. + } + + @Override + public String toString() { + return "Recipe{" + + "name='" + name + '\'' + + ", ingredientsCount=" + ingredients.size() + + ", preparationStepsCount=" + preparationSteps.size() + + "}"; + } + + @SuppressWarnings("unused") + public String toDetailedString() { + // More detailed toString for debugging. + return "Recipe{" + + "name='" + name + '\'' + + ", ingredients=" + ingredients + + ", preparationSteps=" + preparationSteps + + '}'; + } + +} \ No newline at end of file From ccdccffee8e74521b7c206778db25eb24718ddda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Rasie=C5=84ski?= Date: Fri, 21 Nov 2025 19:35:03 +0100 Subject: [PATCH 03/15] Added RecipeRepository --- .../server/database/RecipeRepository.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 server/src/main/java/server/database/RecipeRepository.java diff --git a/server/src/main/java/server/database/RecipeRepository.java b/server/src/main/java/server/database/RecipeRepository.java new file mode 100644 index 0000000..60ec130 --- /dev/null +++ b/server/src/main/java/server/database/RecipeRepository.java @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Delft University of Technology + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package server.database; + +import org.springframework.data.jpa.repository.JpaRepository; + +import commons.Recipe; + +public interface RecipeRepository extends JpaRepository {} \ No newline at end of file From 110c8e72d4089447a581140096e61c6b194d0306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Rasie=C5=84ski?= Date: Fri, 21 Nov 2025 19:38:48 +0100 Subject: [PATCH 04/15] Removed * import --- commons/src/main/java/commons/Recipe.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/commons/src/main/java/commons/Recipe.java b/commons/src/main/java/commons/Recipe.java index 951fc85..0a8cfaa 100644 --- a/commons/src/main/java/commons/Recipe.java +++ b/commons/src/main/java/commons/Recipe.java @@ -15,7 +15,17 @@ */ package commons; -import jakarta.persistence.*; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Entity; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OrderColumn; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.ElementCollection; + import java.util.ArrayList; import java.util.Collections; From 873f1911ac9b6037847424b55cd3090a99600459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Rasie=C5=84ski?= Date: Fri, 21 Nov 2025 20:17:40 +0100 Subject: [PATCH 05/15] Added tests for Recipe. --- commons/src/test/java/commons/RecipeTest.java | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 commons/src/test/java/commons/RecipeTest.java diff --git a/commons/src/test/java/commons/RecipeTest.java b/commons/src/test/java/commons/RecipeTest.java new file mode 100644 index 0000000..e53ac58 --- /dev/null +++ b/commons/src/test/java/commons/RecipeTest.java @@ -0,0 +1,128 @@ +package commons; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class RecipeTest { + + Recipe recipe; + + @BeforeEach + void setupRecipe() { + this.recipe = new Recipe(1L, "Chocolate Cake"); + } + + @Test + void getId() { + assertEquals(1L, recipe.getId()); + } + + @Test + void setId() { + recipe.setId(2048L); + assertEquals(2048L, recipe.getId()); + } + + @Test + void getName() { + assertEquals("Chocolate Cake", recipe.getName()); + } + + @Test + void setName() { + recipe.setName("Steak"); + assertEquals("Steak", recipe.getName()); + } + + @Test + void getIngredientsAddThrow() { + // TODO: Change to actual Ingredient class later + assertThrows(UnsupportedOperationException.class, () -> { + recipe.getIngredients().add("Lasagna"); + }); + } + + @Test + void getIngredientsClearThrow() { + assertThrows(UnsupportedOperationException.class, () -> { + recipe.getIngredients().clear(); + }); + } + + @Test + void setIngredients() { + // TODO: Change to actual Ingredient class later + List ingredients = new ArrayList<>(List.of("Chocolate", "Flour", "Egg")); + recipe.setIngredients(ingredients); + + assertEquals(recipe.getIngredients(), ingredients); + assertEquals(recipe.getIngredients().size(), 3); + } + + @Test + void getPreparationStepsAddThrow() { + assertThrows(UnsupportedOperationException.class, () -> { + recipe.getPreparationSteps().add("Preheat Oven"); + }); + } + + @Test + void getPreparationStepsClearThrow() { + assertThrows(UnsupportedOperationException.class, () -> { + recipe.getPreparationSteps().clear(); + }); + } + + @Test + void setPreparationSteps() { + List steps = new ArrayList<>(List.of("Preheat oven", "Mix stuff", "decorate")); + recipe.setPreparationSteps(steps); + + assertEquals(recipe.getPreparationSteps(), steps); + assertEquals(recipe.getPreparationSteps().size(), 3); + } + + @Test + void testEqualsSame() { + Recipe recipe2 = new Recipe(1L, "Chocolate Cake"); + assertEquals(recipe, recipe2); + } + + @Test + void testEqualsSameExceptId() { + Recipe recipe2 = new Recipe(2L, "Chocolate Cake"); + assertNotEquals(recipe, recipe2); + } + + @Test + void testEqualsOnlySameId() { + Recipe recipe2 = new Recipe(1L, "Some random recipe"); + assertEquals(recipe, recipe2); // Equals, we only look at ID!!! + } + + @Test + void testHashCodeSame() { + Recipe recipe2 = new Recipe(1L, "Chocolate Cake"); + + assertEquals(recipe.hashCode(), recipe2.hashCode()); + } + + @Test + void testHashCodeSameExceptId() { + Recipe recipe2 = new Recipe(2L, "Chocolate Cake"); + + assertNotEquals(recipe.hashCode(), recipe2.hashCode()); + } + + @Test + void testHashCodeOnlySameId() { + Recipe recipe2 = new Recipe(1L, "Some random recipe"); + + assertEquals(recipe.hashCode(), recipe2.hashCode()); // Same, only looks at ID!!! + } +} \ No newline at end of file From 1273ae47fe37eb8e646b06dc7199805088647222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Rasie=C5=84ski?= Date: Fri, 21 Nov 2025 20:18:31 +0100 Subject: [PATCH 06/15] Expression lambdas --- commons/src/test/java/commons/RecipeTest.java | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/commons/src/test/java/commons/RecipeTest.java b/commons/src/test/java/commons/RecipeTest.java index e53ac58..97afd8b 100644 --- a/commons/src/test/java/commons/RecipeTest.java +++ b/commons/src/test/java/commons/RecipeTest.java @@ -42,16 +42,12 @@ class RecipeTest { @Test void getIngredientsAddThrow() { // TODO: Change to actual Ingredient class later - assertThrows(UnsupportedOperationException.class, () -> { - recipe.getIngredients().add("Lasagna"); - }); + assertThrows(UnsupportedOperationException.class, () -> recipe.getIngredients().add("Lasagna")); } @Test void getIngredientsClearThrow() { - assertThrows(UnsupportedOperationException.class, () -> { - recipe.getIngredients().clear(); - }); + assertThrows(UnsupportedOperationException.class, () -> recipe.getIngredients().clear()); } @Test @@ -66,16 +62,12 @@ class RecipeTest { @Test void getPreparationStepsAddThrow() { - assertThrows(UnsupportedOperationException.class, () -> { - recipe.getPreparationSteps().add("Preheat Oven"); - }); + assertThrows(UnsupportedOperationException.class, () -> recipe.getPreparationSteps().add("Preheat Oven")); } @Test void getPreparationStepsClearThrow() { - assertThrows(UnsupportedOperationException.class, () -> { - recipe.getPreparationSteps().clear(); - }); + assertThrows(UnsupportedOperationException.class, () -> recipe.getPreparationSteps().clear()); } @Test From a4ec14bfb0b1cb9e208657e506445ac2dd283d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Rasie=C5=84ski?= Date: Fri, 21 Nov 2025 20:21:11 +0100 Subject: [PATCH 07/15] Removed magic numbers from tests --- commons/src/test/java/commons/RecipeTest.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/commons/src/test/java/commons/RecipeTest.java b/commons/src/test/java/commons/RecipeTest.java index 97afd8b..138b79c 100644 --- a/commons/src/test/java/commons/RecipeTest.java +++ b/commons/src/test/java/commons/RecipeTest.java @@ -11,21 +11,22 @@ import static org.junit.jupiter.api.Assertions.*; class RecipeTest { Recipe recipe; + final Long RECIPE_ID = 1L; @BeforeEach void setupRecipe() { - this.recipe = new Recipe(1L, "Chocolate Cake"); + this.recipe = new Recipe(RECIPE_ID, "Chocolate Cake"); } @Test void getId() { - assertEquals(1L, recipe.getId()); + assertEquals(RECIPE_ID, recipe.getId()); } @Test void setId() { - recipe.setId(2048L); - assertEquals(2048L, recipe.getId()); + recipe.setId(RECIPE_ID + 1); + assertEquals(RECIPE_ID + 1, recipe.getId()); } @Test @@ -57,7 +58,7 @@ class RecipeTest { recipe.setIngredients(ingredients); assertEquals(recipe.getIngredients(), ingredients); - assertEquals(recipe.getIngredients().size(), 3); + assertEquals(recipe.getIngredients().size(), ingredients.size()); } @Test @@ -76,44 +77,44 @@ class RecipeTest { recipe.setPreparationSteps(steps); assertEquals(recipe.getPreparationSteps(), steps); - assertEquals(recipe.getPreparationSteps().size(), 3); + assertEquals(recipe.getPreparationSteps().size(), steps.size()); } @Test void testEqualsSame() { - Recipe recipe2 = new Recipe(1L, "Chocolate Cake"); + Recipe recipe2 = new Recipe(RECIPE_ID, "Chocolate Cake"); assertEquals(recipe, recipe2); } @Test void testEqualsSameExceptId() { - Recipe recipe2 = new Recipe(2L, "Chocolate Cake"); + Recipe recipe2 = new Recipe(RECIPE_ID + 1, "Chocolate Cake"); assertNotEquals(recipe, recipe2); } @Test void testEqualsOnlySameId() { - Recipe recipe2 = new Recipe(1L, "Some random recipe"); + Recipe recipe2 = new Recipe(RECIPE_ID, "Some random recipe"); assertEquals(recipe, recipe2); // Equals, we only look at ID!!! } @Test void testHashCodeSame() { - Recipe recipe2 = new Recipe(1L, "Chocolate Cake"); + Recipe recipe2 = new Recipe(RECIPE_ID, "Chocolate Cake"); assertEquals(recipe.hashCode(), recipe2.hashCode()); } @Test void testHashCodeSameExceptId() { - Recipe recipe2 = new Recipe(2L, "Chocolate Cake"); + Recipe recipe2 = new Recipe(RECIPE_ID + 1, "Chocolate Cake"); assertNotEquals(recipe.hashCode(), recipe2.hashCode()); } @Test void testHashCodeOnlySameId() { - Recipe recipe2 = new Recipe(1L, "Some random recipe"); + Recipe recipe2 = new Recipe(RECIPE_ID, "Some random recipe"); assertEquals(recipe.hashCode(), recipe2.hashCode()); // Same, only looks at ID!!! } From 2d9af65f2872ec49692de0743224b6465fa4e405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Rasie=C5=84ski?= Date: Fri, 21 Nov 2025 20:25:12 +0100 Subject: [PATCH 08/15] fixed checkstyle issue? --- commons/src/test/java/commons/RecipeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commons/src/test/java/commons/RecipeTest.java b/commons/src/test/java/commons/RecipeTest.java index 138b79c..db70bf2 100644 --- a/commons/src/test/java/commons/RecipeTest.java +++ b/commons/src/test/java/commons/RecipeTest.java @@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.*; class RecipeTest { Recipe recipe; - final Long RECIPE_ID = 1L; + static final Long RECIPE_ID = 1L; @BeforeEach void setupRecipe() { From 4e23d913d14d144aca0b92387b666c9448d8111d Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Sat, 22 Nov 2025 00:47:43 +0100 Subject: [PATCH 09/15] feat: RecipeController with GET, POST, PUT, DELETE TODO: WebSockets updates should be propagated after POST/PUT/DELETE operations --- .../java/server/api/RecipeController.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 server/src/main/java/server/api/RecipeController.java diff --git a/server/src/main/java/server/api/RecipeController.java b/server/src/main/java/server/api/RecipeController.java new file mode 100644 index 0000000..4a81057 --- /dev/null +++ b/server/src/main/java/server/api/RecipeController.java @@ -0,0 +1,121 @@ +package server.api; + +import commons.Recipe; + +import org.springframework.data.domain.PageRequest; +import org.springframework.http.ResponseEntity; + +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import server.database.RecipeRepository; + +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/recipe") +public class RecipeController { + private final RecipeRepository recipeRepository; // JPA repository used in this controller + + public RecipeController(RecipeRepository recipeRepository) { + this.recipeRepository = recipeRepository; + } + + /** + * Mapping for GET /recipe/{id} + *

+ * Gets a specific recipe by its unique id. + *

+ * @param id id of the recipe + * @return The recipe if it exists in the repository; otherwise returns 404 Not Found status + */ + @GetMapping("/recipe/{id}") + public ResponseEntity getRecipe(@PathVariable Long id) { + if (!recipeRepository.existsById(id)) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(recipeRepository.findById(id).get()); + } + + /** + * Mapping for GET /recipes(?limit=) + *

+ * If the limit parameter is unspecified, return all recipes in the repository. + * @param limit Integer limit of items you want to get + * @return The list of recipes + */ + @GetMapping("/recipes") + public ResponseEntity> getRecipes(@RequestParam Optional limit) { + if (limit.isPresent()) { + return ResponseEntity.ok( + recipeRepository.findAll( + PageRequest.of(0, limit.get()) + ).toList()); + } + return ResponseEntity.ok(recipeRepository.findAll()); + } + + /** + * Mapping for POST /recipe/{id} + * @param id The recipe id to replace + * @param recipe The new recipe to be replaced from the original + * @return The changed recipe; returns 400 Bad Request if the recipe does not exist + */ + @PostMapping("/recipe/{id}") + public ResponseEntity updateRecipe(@PathVariable Long id, @RequestBody Recipe recipe) { + if (!recipeRepository.existsById(id)) { + return ResponseEntity.badRequest().build(); + } + + // TODO: Send WS update to all subscribers with the updated recipe + + return ResponseEntity.ok(recipeRepository.save(recipe)); + } + + /** + * Mapping for PUT /recipe/new + *

+ * Inserts a new recipe into the repository + *

+ * @param recipe The new recipe as a request body + * @return 200 OK with the recipe you added; or 400 Bad Request if the recipe already exists + */ + @PutMapping("/recipe/new") + public ResponseEntity createRecipe(@RequestBody Recipe recipe) { + if (recipeRepository.existsById(recipe.getId())) { + return ResponseEntity.badRequest().build(); + } + + // TODO: Send WS update to all subscribers with the new recipe + + return ResponseEntity.ok(recipeRepository.save(recipe)); + } + + /** + * Mapping for DELETE /recipe/{id} + *

+ * Deletes a recipe identified by its id. + *

+ * @param id The id of the recipe to be deleted. + * @return 200 OK with the recipe that was deleted; or 400 Bad Request if the recipe doesn't exist. + */ + @DeleteMapping("/recipe/{id}") + public ResponseEntity deleteRecipe(@PathVariable Long id) { + if (!recipeRepository.existsById(id)) { + return ResponseEntity.badRequest().build(); + } + Recipe removedRecipe = recipeRepository.findById(id).get(); + recipeRepository.delete(removedRecipe); + + // TODO: Send WS update to propagate deletion + return ResponseEntity.ok(removedRecipe); + } +} From 0a9251df67729eb4f9c0b76f2ce2db93bf77b0c9 Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Sat, 22 Nov 2025 01:14:29 +0100 Subject: [PATCH 10/15] fix: recipe api now under /api --- server/src/main/java/server/api/RecipeController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/server/api/RecipeController.java b/server/src/main/java/server/api/RecipeController.java index 4a81057..09fbe56 100644 --- a/server/src/main/java/server/api/RecipeController.java +++ b/server/src/main/java/server/api/RecipeController.java @@ -21,7 +21,7 @@ import java.util.List; import java.util.Optional; @RestController -@RequestMapping("/recipe") +@RequestMapping("/api") public class RecipeController { private final RecipeRepository recipeRepository; // JPA repository used in this controller From f62e836692c1cb8282ebf0c906ef5da341eadb43 Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Sat, 22 Nov 2025 01:16:38 +0100 Subject: [PATCH 11/15] fix: createRecipe checks name is unique Uses JPA Repository.exists(Example); Changed no-arg constuctor of Recipe to public. --- commons/src/main/java/commons/Recipe.java | 2 +- .../src/main/java/server/api/RecipeController.java | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/commons/src/main/java/commons/Recipe.java b/commons/src/main/java/commons/Recipe.java index 0a8cfaa..6ee2497 100644 --- a/commons/src/main/java/commons/Recipe.java +++ b/commons/src/main/java/commons/Recipe.java @@ -83,7 +83,7 @@ public class Recipe { private List preparationSteps = new ArrayList<>(); @SuppressWarnings("unused") - private Recipe() { + public Recipe() { // for object mapper } diff --git a/server/src/main/java/server/api/RecipeController.java b/server/src/main/java/server/api/RecipeController.java index 09fbe56..9b1753e 100644 --- a/server/src/main/java/server/api/RecipeController.java +++ b/server/src/main/java/server/api/RecipeController.java @@ -2,6 +2,7 @@ package server.api; import commons.Recipe; +import org.springframework.data.domain.Example; import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; @@ -86,11 +87,20 @@ public class RecipeController { * Inserts a new recipe into the repository *

* @param recipe The new recipe as a request body - * @return 200 OK with the recipe you added; or 400 Bad Request if the recipe already exists + * @return 200 OK with the recipe you added; or 400 Bad Request if the recipe already exists by name */ @PutMapping("/recipe/new") public ResponseEntity createRecipe(@RequestBody Recipe recipe) { - if (recipeRepository.existsById(recipe.getId())) { + + // We initialize a new example recipe with the name of input recipe + // This is the only attribute we are concerned about making sure it's unique + Recipe example = new Recipe(); + example.setName(recipe.getName()); + + /* Here we use very funny JPA magic repository.exists(Example) + We check if any recipe in the repository has the same name as the input + */ + if (recipeRepository.exists(Example.of(example))) { return ResponseEntity.badRequest().build(); } From 17f4b8bba4143b845d7bd37c5a4c4a8706335a16 Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Sat, 22 Nov 2025 01:24:04 +0100 Subject: [PATCH 12/15] fix: correct deletion method implementation reference: https://stackoverflow.com/questions/75912878/could-not-write-json-failed-to-lazily-initialize-a-collection-when-returning-an --- server/src/main/java/server/api/RecipeController.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/server/api/RecipeController.java b/server/src/main/java/server/api/RecipeController.java index 9b1753e..a1d79fe 100644 --- a/server/src/main/java/server/api/RecipeController.java +++ b/server/src/main/java/server/api/RecipeController.java @@ -115,17 +115,16 @@ public class RecipeController { * Deletes a recipe identified by its id. *

* @param id The id of the recipe to be deleted. - * @return 200 OK with the recipe that was deleted; or 400 Bad Request if the recipe doesn't exist. + * @return 200 OK with true; or 400 Bad Request if the recipe doesn't exist. */ @DeleteMapping("/recipe/{id}") - public ResponseEntity deleteRecipe(@PathVariable Long id) { + public ResponseEntity deleteRecipe(@PathVariable Long id) { if (!recipeRepository.existsById(id)) { return ResponseEntity.badRequest().build(); } - Recipe removedRecipe = recipeRepository.findById(id).get(); - recipeRepository.delete(removedRecipe); + recipeRepository.deleteById(id); // TODO: Send WS update to propagate deletion - return ResponseEntity.ok(removedRecipe); + return ResponseEntity.ok(true); } } From a1fbea1585c2ee2f4eb55829c32f5f13bf9c323b Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Sat, 22 Nov 2025 02:35:00 +0100 Subject: [PATCH 13/15] test: add unit tests for RecipeController Covers 6/6 methods in controller Also added test configuration in application-mock-data-test.properties --- .../application-mock-data-test.properties | 18 +++ .../java/server/api/RecipeControllerTest.java | 137 ++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 server/src/main/resources/application-mock-data-test.properties create mode 100644 server/src/test/java/server/api/RecipeControllerTest.java diff --git a/server/src/main/resources/application-mock-data-test.properties b/server/src/main/resources/application-mock-data-test.properties new file mode 100644 index 0000000..c68d6fa --- /dev/null +++ b/server/src/main/resources/application-mock-data-test.properties @@ -0,0 +1,18 @@ +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect + +# use one of these alternatives... +# ... purely in-memory, wiped on restart, but great for testing +spring.datasource.url=jdbc:h2:mem:testdb +# ... persisted on disk (in project directory) +#spring.datasource.url=jdbc:h2:file:./h2-database + +# enable DB view on http://localhost:8080/h2-console +spring.h2.console.enabled=true + +# strategy for table (re-)generation +spring.jpa.hibernate.ddl-auto=update +# show auto-generated SQL commands +#spring.jpa.hibernate.show_sql=true diff --git a/server/src/test/java/server/api/RecipeControllerTest.java b/server/src/test/java/server/api/RecipeControllerTest.java new file mode 100644 index 0000000..5931b3a --- /dev/null +++ b/server/src/test/java/server/api/RecipeControllerTest.java @@ -0,0 +1,137 @@ +package server.api; + + +import commons.Recipe; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import server.database.RecipeRepository; + +import java.util.List; +import java.util.Optional; +import java.util.stream.LongStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +// Spring Boot unit testing magic +// Before each test the state of the repository is reset by +// rolling back transaction of changes from previous test +@DataJpaTest + +// This test sources its application profile from +// resources/application-mock-data-test.properties +// This config uses an in-memory database +@ActiveProfiles("mock-data-test") +public class RecipeControllerTest { + private RecipeController controller; + private List recipes; + private RecipeRepository recipeRepository; + public static final int NUM_RECIPES = 10; + + // Injects a test repository into the test class + @Autowired + public RecipeControllerTest(RecipeRepository recipeRepository) { + this.recipeRepository = recipeRepository; + } + + @BeforeEach + public void setup() { + recipes = LongStream + .range(0, NUM_RECIPES) + .mapToObj(x -> new Recipe(null, "Recipe " + x, List.of(), List.of())) + .toList(); + controller = new RecipeController(recipeRepository); + } + @Test + public void createOneRecipe() { + controller.createRecipe(recipes.getFirst()); + + // There is 1 recipe in the repository + assertEquals(1, recipeRepository.count()); + } + @Test + public void createManyRecipes() { + recipes.forEach(recipe -> controller.createRecipe(recipe)); + + // There are the same number of recipes in the repository as the input list + assertEquals(recipes.size(), recipeRepository.count()); + } + @Test + public void getManyRecipes() { + recipes.forEach(recipe -> controller.createRecipe(recipe)); + + // The number of recipes returned is the same as the entire input list + assertEquals(recipes.size(), controller.getRecipes(Optional.empty()).getBody().size()); + } + @Test + public void getSomeRecipes() { + final int LIMIT = 5; + recipes.forEach(recipe -> controller.createRecipe(recipe)); + // The number of recipes returned is the same as the entire input list + assertEquals(LIMIT, controller.getRecipes(Optional.of(LIMIT)).getBody().size()); + } + @Test + public void findOneRecipeExists() { + final int CHECK_INDEX = 3; + List ids = LongStream + .range(0, NUM_RECIPES) + .map(recipe -> controller.createRecipe(recipes.get((int) recipe)).getBody().getId()) + .boxed().toList(); + + // The third item in the input list is the same as the third item retrieved from the database + assertEquals( + recipes.get(CHECK_INDEX), + controller.getRecipe(ids.get(CHECK_INDEX)).getBody()); + } + @Test + public void findOneRecipeNotExists() { + final int CHECK_INDEX = 3; + // There does not exist a recipe with ID=3 since there are no items in the repository. + assertEquals( + HttpStatus.NOT_FOUND, + controller.getRecipe((long) CHECK_INDEX).getStatusCode()); + } + @Test + public void deleteOneRecipeGood() { + final int DELETE_INDEX = 5; + List ids = LongStream + .range(0, NUM_RECIPES) + .map(recipe -> controller.createRecipe(recipes.get((int) recipe)).getBody().getId()) + .boxed().toList(); + + // The object has been successfully deleted + assertEquals(HttpStatus.OK, controller.deleteRecipe(ids.get(DELETE_INDEX)).getStatusCode()); + } + @Test + public void deleteOneRecipeCountGood() { + final int DELETE_INDEX = 5; + List ids = LongStream + .range(0, NUM_RECIPES) + .map(recipe -> controller.createRecipe(recipes.get((int) recipe)).getBody().getId()) + .boxed().toList(); + controller.deleteRecipe(ids.get(DELETE_INDEX)); + // The count of items decreased by 1 after the 5th item has been removed. + assertEquals(ids.size() - 1, recipeRepository.count()); + } + @Test + public void deleteOneRecipeFail() { + final Long DELETE_INDEX = 5L; + assertEquals(HttpStatus.BAD_REQUEST, controller.deleteRecipe(DELETE_INDEX).getStatusCode()); + } + + @Test + public void updateOneRecipeHasNewData() { + final int UPDATE_INDEX = 5; + List ids = LongStream + .range(0, NUM_RECIPES) + .map(recipe -> controller.createRecipe(recipes.get((int) recipe)).getBody().getId()) + .boxed().toList(); + Recipe newRecipe = controller.getRecipe(ids.get(UPDATE_INDEX)).getBody(); + newRecipe.setName("New recipe"); + controller.updateRecipe(newRecipe.getId(), newRecipe); + assertEquals("New recipe", recipeRepository.getReferenceById(ids.get(UPDATE_INDEX)).getName()); + } +} From fff5f2fffa575c97a4b3380dc13bc34d0d47e514 Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Sat, 22 Nov 2025 12:19:00 +0100 Subject: [PATCH 14/15] docs: api definition --- docs/recipe-api.md | 243 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 docs/recipe-api.md diff --git a/docs/recipe-api.md b/docs/recipe-api.md new file mode 100644 index 0000000..e2b9b4f --- /dev/null +++ b/docs/recipe-api.md @@ -0,0 +1,243 @@ +## Recipe API + +Base path: `/api` + +This API allows clients to create, read, update, and delete `Recipe` resources. + +--- + +## Endpoints + +### Get a Recipe by ID + +**GET** `/api/recipe/{id}` + +Retrieves a specific recipe by its unique identifier. + +* **Path Parameters** + + | Name | Type | Required | Description | + | ---- | ------ | -------- | ------------------------------ | + | `id` | `Long` | Yes | The ID of the recipe to fetch. | + +* **Responses** + + | Status Code | Description | + | --------------- | ------------------------------------- | + | `200 OK` | Returns the recipe with the given ID. | + | `404 Not Found` | No recipe exists with that ID. | + +* **Example Request** + + ```bash + curl -X GET "http://SERVER_ADDRESS/api/recipe/1" \ + -H "Accept: application/json" + ``` + +* **Example Response (200)** + + ```json + { + "id": 1, + "name": "Pancakes", + "ingredients": ["Ingredient 1", "Ingredient 2"], + "preparationSteps": ["Step 1", "Step 2"] + } + ``` + +--- + +### List Recipes + +**GET** `/api/recipes` + +Retrieves a list of recipes. Supports optional pagination via a `limit` query parameter. + +* **Query Parameters** + + | Name | Type | Required | Description | + | ------- | --------- | -------- | ---------------------------------------------------------------------------- | + | `limit` | `Integer` | No | Maximum number of recipes to return. If not provided, returns *all* recipes. | + +* **Responses** + + | Status Code | Description | + | ----------- | --------------------------------------------- | + | `200 OK` | Returns a list of recipes (possibly limited). | + +* **Example Request (no limit)** + + ```bash + curl -X GET "http://SERVER_ADDRESS/api/recipes" \ + -H "Accept: application/json" + ``` + +* **Example Request (with limit)** + + ```bash + curl -X GET "http://SERVER_ADDRESS/api/recipes?limit=10" \ + -H "Accept: application/json" + ``` + +* **Example Response (200)** + + ```json + [ + { + "id": 10, + "name": "Recipe 0", + "ingredients": ["Flour", "Milk"], + "preparationSteps": ["Do something", "Do something else"] + }, + { + "id": 11, + "name": "Recipe 1", + "ingredients": ["Flour", "Milk"], + "preparationSteps": ["Do something", "Do something else"] + } + // etc. max {limit} items + ] + ``` + +--- + +### Create a New Recipe + +**PUT** `/api/recipe/new` + +Creates a new recipe. The recipe name must be unique in the repository. + +* **Request Body** + + A JSON object representing the recipe. Example: + + ```json + { + "name": "Pancakes", + "ingredients": ["Flour", "Milk", "Eggs"], + "preparationSteps": ["Step 1", "Step 2"] + } + ``` + +* **Responses** + + | Status Code | Description | + | ----------------- | -------------------------------------------------------------------------------------------- | + | `200 OK` | The recipe was successfully created. Returns the created recipe (including its assigned ID). | + | `400 Bad Request` | A recipe with the same name already exists. | + +* **Example Request** + + ```bash + curl -X PUT "https://SERVER_ADDRESS/api/recipe/new" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Pancakes", + "ingredients": ["Flour", "Milk", "Eggs"], + "preparationSteps": ["Step 1", "Step 2"] + }' + ``` + +* **Example Response (200)** + + ```json + { + "id": 125, + "name": "Pancakes", + "ingredients": ["Flour", "Milk", "Eggs"], + "preparationSteps": ["Step 1", "Step 2"] + } + ``` + +--- + +### Update an Existing Recipe + +**POST** `/api/recipe/{id}` + +Replaces or updates the recipe with the given ID. + +* **Path Parameters** + + | Name | Type | Required | Description | + | ---- | ------ | -------- | ------------------------------- | + | `id` | `Long` | Yes | The ID of the recipe to update. | + +* **Request Body** + + A JSON object containing the new recipe data. It is expected to include the ID (or the server-side save will override it), or at least map correctly to the stored entity. + + Example: + + ```json + { + "id": 123, + "name": "Better Pancakes", + "ingredients": ["Flour", "Almond milk", "Eggs"], + "preparationSteps": ["Step 10", "Step 11"] + } + ``` + +* **Responses** + + | Status Code | Description | + | ----------------- | ---------------------------------------------------------------- | + | `200 OK` | The recipe was successfully updated. Returns the updated recipe. | + | `400 Bad Request` | No recipe exists with the given ID. | + + * **Example Request** + + ```bash + curl -X POST "https://your-domain.com/api/recipe/123" \ + -H "Content-Type: application/json" \ + -d '{ + "id": 123, + "name": "Better Pancakes", + "ingredients": ["Flour", "Almond milk", "Eggs"], + "preparationSteps": ["Step 10", "Step 11"] + }' + ``` + +* **Example Response (200)** + + ```json + { + "id": 123, + "name": "Updated Pancakes", + "ingredients": ["Flour", "Almond milk", "Eggs"], + "preparationSteps": "Mix and fry differently." + } + ``` + +--- + +### Delete a Recipe + +**DELETE** `/api/recipe/{id}` + +Deletes the recipe with the given ID. + +* **Path Parameters** + + | Name | Type | Required | Description | + | ---- | ------ | -------- | ------------------------------- | + | `id` | `Long` | Yes | The ID of the recipe to delete. | + +* **Responses** + + | Status Code | Description | + | ----------------- | ---------------------------------------------------- | + | `200 OK` | The recipe was successfully deleted. Returns `true`. | + | `400 Bad Request` | No recipe exists with the given ID. | + +* **Example Request** + + ```bash + curl -X DELETE "https://your-domain.com/api/recipe/123" + ``` + +* **Example Response (200)** + + ```json + true + ``` \ No newline at end of file From 1e9bd169d06cd219e9a20bf606053ed1c6fadafd Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Sun, 23 Nov 2025 20:04:59 +0100 Subject: [PATCH 15/15] test: improve unit test clarity --- .../java/server/api/RecipeControllerTest.java | 68 +++++++++++-------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/server/src/test/java/server/api/RecipeControllerTest.java b/server/src/test/java/server/api/RecipeControllerTest.java index 5931b3a..f1a766e 100644 --- a/server/src/test/java/server/api/RecipeControllerTest.java +++ b/server/src/test/java/server/api/RecipeControllerTest.java @@ -3,15 +3,19 @@ package server.api; import commons.Recipe; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.http.HttpStatus; import org.springframework.test.context.ActiveProfiles; import server.database.RecipeRepository; +import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.LongStream; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -28,7 +32,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class RecipeControllerTest { private RecipeController controller; private List recipes; - private RecipeRepository recipeRepository; + private final RecipeRepository recipeRepository; + private List recipeIds; public static final int NUM_RECIPES = 10; // Injects a test repository into the test class @@ -38,12 +43,29 @@ public class RecipeControllerTest { } @BeforeEach - public void setup() { + public void setup(TestInfo info) { recipes = LongStream .range(0, NUM_RECIPES) .mapToObj(x -> new Recipe(null, "Recipe " + x, List.of(), List.of())) .toList(); controller = new RecipeController(recipeRepository); + Set tags = info.getTags(); + List ids = new ArrayList<>(); + + // Some tests want initial data to be created. + if (tags.contains("test-from-init-data")) { + ids = LongStream + .range(0, NUM_RECIPES) + .map(idx -> recipeRepository.save(recipes.get((int) idx)).getId()) + .boxed().toList(); + } + + // Some tests need to know the stored IDs of objects + if (tags.contains("need-ids")) { + recipeIds = ids; + } + + // If no tags specified, the repository is initialized as empty. } @Test public void createOneRecipe() { @@ -60,31 +82,27 @@ public class RecipeControllerTest { assertEquals(recipes.size(), recipeRepository.count()); } @Test + @Tag("test-from-init-data") public void getManyRecipes() { - recipes.forEach(recipe -> controller.createRecipe(recipe)); - // The number of recipes returned is the same as the entire input list assertEquals(recipes.size(), controller.getRecipes(Optional.empty()).getBody().size()); } @Test + @Tag("test-from-init-data") public void getSomeRecipes() { final int LIMIT = 5; - recipes.forEach(recipe -> controller.createRecipe(recipe)); // The number of recipes returned is the same as the entire input list assertEquals(LIMIT, controller.getRecipes(Optional.of(LIMIT)).getBody().size()); } @Test + @Tag("test-from-init-data") + @Tag("need-ids") public void findOneRecipeExists() { final int CHECK_INDEX = 3; - List ids = LongStream - .range(0, NUM_RECIPES) - .map(recipe -> controller.createRecipe(recipes.get((int) recipe)).getBody().getId()) - .boxed().toList(); - // The third item in the input list is the same as the third item retrieved from the database assertEquals( recipes.get(CHECK_INDEX), - controller.getRecipe(ids.get(CHECK_INDEX)).getBody()); + controller.getRecipe(recipeIds.get(CHECK_INDEX)).getBody()); } @Test public void findOneRecipeNotExists() { @@ -95,26 +113,22 @@ public class RecipeControllerTest { controller.getRecipe((long) CHECK_INDEX).getStatusCode()); } @Test + @Tag("test-from-init-data") + @Tag("need-ids") public void deleteOneRecipeGood() { final int DELETE_INDEX = 5; - List ids = LongStream - .range(0, NUM_RECIPES) - .map(recipe -> controller.createRecipe(recipes.get((int) recipe)).getBody().getId()) - .boxed().toList(); // The object has been successfully deleted - assertEquals(HttpStatus.OK, controller.deleteRecipe(ids.get(DELETE_INDEX)).getStatusCode()); + assertEquals(HttpStatus.OK, controller.deleteRecipe(recipeIds.get(DELETE_INDEX)).getStatusCode()); } @Test + @Tag("test-from-init-data") + @Tag("need-ids") public void deleteOneRecipeCountGood() { final int DELETE_INDEX = 5; - List ids = LongStream - .range(0, NUM_RECIPES) - .map(recipe -> controller.createRecipe(recipes.get((int) recipe)).getBody().getId()) - .boxed().toList(); - controller.deleteRecipe(ids.get(DELETE_INDEX)); + controller.deleteRecipe(recipeIds.get(DELETE_INDEX)); // The count of items decreased by 1 after the 5th item has been removed. - assertEquals(ids.size() - 1, recipeRepository.count()); + assertEquals(recipeIds.size() - 1, recipeRepository.count()); } @Test public void deleteOneRecipeFail() { @@ -123,15 +137,13 @@ public class RecipeControllerTest { } @Test + @Tag("test-from-init-data") + @Tag("need-ids") public void updateOneRecipeHasNewData() { final int UPDATE_INDEX = 5; - List ids = LongStream - .range(0, NUM_RECIPES) - .map(recipe -> controller.createRecipe(recipes.get((int) recipe)).getBody().getId()) - .boxed().toList(); - Recipe newRecipe = controller.getRecipe(ids.get(UPDATE_INDEX)).getBody(); + Recipe newRecipe = controller.getRecipe(recipeIds.get(UPDATE_INDEX)).getBody(); newRecipe.setName("New recipe"); controller.updateRecipe(newRecipe.getId(), newRecipe); - assertEquals("New recipe", recipeRepository.getReferenceById(ids.get(UPDATE_INDEX)).getName()); + assertEquals("New recipe", recipeRepository.getReferenceById(recipeIds.get(UPDATE_INDEX)).getName()); } }