From 3aa530ffde20028d60b8cb458c8c678845141b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Rasie=C5=84ski?= Date: Tue, 25 Nov 2025 17:46:15 +0100 Subject: [PATCH] feat: basic Recipe class and RecipeController with GET, POST, PUT, DELETE --- 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 insertions(+) create mode 100644 commons/src/main/java/commons/Recipe.java create mode 100644 commons/src/test/java/commons/RecipeTest.java create mode 100644 docs/recipe-api.md create mode 100644 server/src/main/java/server/api/RecipeController.java create mode 100644 server/src/main/java/server/database/RecipeRepository.java 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/commons/src/main/java/commons/Recipe.java b/commons/src/main/java/commons/Recipe.java new file mode 100644 index 0000000..0472175 --- /dev/null +++ b/commons/src/main/java/commons/Recipe.java @@ -0,0 +1,216 @@ +/* + * 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 new file mode 100644 index 0000000..db70bf2 --- /dev/null +++ b/commons/src/test/java/commons/RecipeTest.java @@ -0,0 +1,121 @@ +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 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 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..a1d79fe --- /dev/null +++ b/server/src/main/java/server/api/RecipeController.java @@ -0,0 +1,130 @@ +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 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 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..f1a766e --- /dev/null +++ b/server/src/test/java/server/api/RecipeControllerTest.java @@ -0,0 +1,149 @@ +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()); + } +}