feat: ingredient controller, websocket messages, ingredient controller tests
This commit is contained in:
parent
6129fc2a5a
commit
3af808ef58
13 changed files with 498 additions and 9 deletions
|
|
@ -47,6 +47,12 @@
|
|||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.hk2</groupId>
|
||||
<artifactId>hk2-api</artifactId>
|
||||
<version>3.0.6</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
|
|
|||
205
server/src/main/java/server/api/IngredientController.java
Normal file
205
server/src/main/java/server/api/IngredientController.java
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
package server.api;
|
||||
|
||||
import commons.Ingredient;
|
||||
import commons.ws.Topics;
|
||||
import commons.ws.messages.CreateIngredientMessage;
|
||||
import commons.ws.messages.DeleteIngredientMessage;
|
||||
import commons.ws.messages.UpdateIngredientMessage;
|
||||
import org.springframework.data.domain.Example;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
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.IngredientRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* REST controller for managing ingredients.
|
||||
* <p>
|
||||
* Exposes the following endpoints:
|
||||
* <ul>
|
||||
* <li>GET /api/ingredients - Get a list of ingredients, optionally paginated.</li>
|
||||
* <li>GET /api/ingredients/{id} - Get a specific ingredient by its ID.</li>
|
||||
* <li>POST /api/ingredients - Create a new ingredient.</li>
|
||||
* <li>PATCH /api/ingredients/{id} - Update an existing ingredient by its ID.</li>
|
||||
* <li>DELETE /api/ingredients/{id} - Delete an ingredient by its ID.</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
* @see Ingredient
|
||||
* @see IngredientRepository
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class IngredientController {
|
||||
private final IngredientRepository ingredientRepository;
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
public IngredientController(IngredientRepository ingredientRepository,
|
||||
SimpMessagingTemplate messagingTemplate) {
|
||||
this.ingredientRepository = ingredientRepository;
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of ingredients, optionally paginated. <br />
|
||||
* Maps to <code>GET /api/ingredients(?page=&limit=)</code>
|
||||
* <p>
|
||||
* If no limit is specified, all ingredients are returned sorted by name ascending.
|
||||
* When calling this function, consider using pagination to avoid large responses.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* When using pagination, you should provide a limit.
|
||||
* The page defaults to 0 if not provided.
|
||||
* </p>
|
||||
*
|
||||
* @param page The page number to retrieve (0-indexed). Optional.
|
||||
* @param limit The maximum number of ingredients to return. Optional.
|
||||
* @return A ResponseEntity containing the list of ingredients.
|
||||
*
|
||||
* @see Ingredient
|
||||
*/
|
||||
@GetMapping("/ingredients")
|
||||
public ResponseEntity<List<Ingredient>> getIngredients(
|
||||
@RequestParam Optional<Integer> page,
|
||||
@RequestParam Optional<Integer> limit
|
||||
) {
|
||||
List<Ingredient> ingredients = limit
|
||||
.map(l -> {
|
||||
return ingredientRepository.findAllByOrderByNameAsc
|
||||
(PageRequest.of(page.orElse(0), l)).toList();
|
||||
})
|
||||
.orElseGet(ingredientRepository::findAllByOrderByNameAsc);
|
||||
|
||||
return ResponseEntity.ok(ingredients);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific ingredient by its ID.
|
||||
* Maps to <code>GET /api/ingredients/{id}</code>
|
||||
*
|
||||
* <p>
|
||||
* Returns 200 OK with the ingredient if found,
|
||||
* or 404 Not Found if the ingredient does not exist.
|
||||
* </p>
|
||||
*
|
||||
* @param id The ID of the ingredient to retrieve.
|
||||
* @return The ingredient wrapped in a ResponseEntity.
|
||||
*
|
||||
* @see Ingredient
|
||||
*/
|
||||
@GetMapping("/ingredients/{id}")
|
||||
public ResponseEntity<Ingredient> getIngredientById(@PathVariable Long id) {
|
||||
return ingredientRepository.findById(id)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing ingredient by its ID.
|
||||
* Maps to <code>PATCH /api/ingredients/{id}</code>
|
||||
*
|
||||
* <p>
|
||||
* If the ingredient with the specified ID does not exist,
|
||||
* returns 404 Not Found.
|
||||
* <p>
|
||||
* If the ingredient exists, updates it with the provided data
|
||||
* and returns the updated ingredient with 200 OK.
|
||||
* </p>
|
||||
*
|
||||
* @param id The ID of the ingredient to update.
|
||||
* @param updated The updated ingredient data.
|
||||
* @return The updated ingredient wrapped in a ResponseEntity.
|
||||
*
|
||||
* @see Ingredient
|
||||
*/
|
||||
@PatchMapping("/ingredients/{id}")
|
||||
public ResponseEntity<Ingredient> updateIngredient(
|
||||
@PathVariable Long id,
|
||||
@RequestBody Ingredient updated
|
||||
) {
|
||||
if (!ingredientRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// TODO: Refactor to use setters
|
||||
updated.id = id;
|
||||
Ingredient savedIngredient = ingredientRepository.save(updated);
|
||||
messagingTemplate.convertAndSend(Topics.INGREDIENTS, new CreateIngredientMessage(savedIngredient));
|
||||
|
||||
return ResponseEntity.ok(savedIngredient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new ingredient.
|
||||
* Maps to <code>POST /api/ingredients</code>
|
||||
*
|
||||
* <p>
|
||||
* If an ingredient with the same name already exists,
|
||||
* returns 400 Bad Request.
|
||||
*
|
||||
* If the ingredient is created successfully,
|
||||
* returns the created ingredient with 200 OK.
|
||||
* </p>
|
||||
*
|
||||
* @param ingredient The ingredient to create.
|
||||
* @return The created ingredient wrapped in a ResponseEntity.
|
||||
*
|
||||
* @see Ingredient
|
||||
*/
|
||||
@PostMapping("/ingredients")
|
||||
public ResponseEntity<Ingredient> createIngredient(@RequestBody Ingredient ingredient) {
|
||||
if (ingredient.name == null || ingredient.name.isEmpty()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
Ingredient example = new Ingredient();
|
||||
example.name = ingredient.name;
|
||||
|
||||
if (ingredientRepository.exists(Example.of(example))) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
Ingredient saved = ingredientRepository.save(ingredient);
|
||||
messagingTemplate.convertAndSend(Topics.INGREDIENTS, new UpdateIngredientMessage(saved));
|
||||
|
||||
return ResponseEntity.ok(saved);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an ingredient by its ID.
|
||||
* Maps to <code>DELETE /api/ingredients/{id}</code>
|
||||
*
|
||||
* <p>
|
||||
* Returns 404 Not Found if the ingredient does not exist.
|
||||
* If the ingredient is deleted successfully, returns 200 OK with true.
|
||||
* </p>
|
||||
*
|
||||
* @param id The ID of the ingredient to delete.
|
||||
* @return A ResponseEntity indicating the result of the operation.
|
||||
*
|
||||
* @see Ingredient
|
||||
*/
|
||||
@DeleteMapping("/ingredients/{id}")
|
||||
public ResponseEntity<Boolean> deleteIngredient(@PathVariable Long id) {
|
||||
if (!ingredientRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
ingredientRepository.deleteById(id);
|
||||
messagingTemplate.convertAndSend(Topics.INGREDIENTS, new DeleteIngredientMessage(id));
|
||||
|
||||
return ResponseEntity.ok(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,8 @@ public class RecipeController {
|
|||
private final RecipeRepository recipeRepository; // JPA repository used in this controller
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
public RecipeController(RecipeRepository recipeRepository, SimpMessagingTemplate messagingTemplate) {
|
||||
public RecipeController(RecipeRepository recipeRepository,
|
||||
SimpMessagingTemplate messagingTemplate) {
|
||||
this.recipeRepository = recipeRepository;
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package server.api;
|
||||
|
||||
import commons.Recipe;
|
||||
import commons.ws.Topics;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
|
|
@ -22,7 +23,7 @@ public class UpdateMessagingController {
|
|||
* @return The updated {@link Recipe} object wrapped in a {@link org.springframework.http.ResponseEntity}.
|
||||
*/
|
||||
@MessageMapping("/updates/recipe")
|
||||
@SendTo("/subscribe/recipe")
|
||||
@SendTo(Topics.RECIPES)
|
||||
public ResponseEntity<Recipe> broadcastRecipeUpdate(Recipe recipe) {
|
||||
return recipeController.updateRecipe(recipe.getId(), recipe);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
package server;
|
||||
package server.database;
|
||||
|
||||
import commons.Ingredient;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IngredientRepository extends JpaRepository<Ingredient, Long> {
|
||||
List<Ingredient> findAllByOrderByNameAsc();
|
||||
Page<Ingredient> findAllByOrderByNameAsc(Pageable pageable);
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package server;
|
||||
package server.database;
|
||||
|
||||
import commons.RecipeIngredient;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
166
server/src/test/java/server/api/IngredientControllerTest.java
Normal file
166
server/src/test/java/server/api/IngredientControllerTest.java
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
package server.api;
|
||||
|
||||
import commons.Ingredient;
|
||||
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.context.annotation.Import;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import server.database.IngredientRepository;
|
||||
import server.WebSocketConfig;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@DataJpaTest
|
||||
@ActiveProfiles("mock-data-test")
|
||||
@Import(WebSocketConfig.class)
|
||||
public class IngredientControllerTest {
|
||||
private final SimpMessagingTemplate template;
|
||||
private final IngredientRepository ingredientRepository;
|
||||
private IngredientController controller;
|
||||
|
||||
@Autowired
|
||||
public IngredientControllerTest(IngredientRepository ingredientRepository,
|
||||
SimpMessagingTemplate template) {
|
||||
this.ingredientRepository = ingredientRepository;
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
private void createInitialIngredients() {
|
||||
ingredientRepository.deleteAll();
|
||||
|
||||
Stream.of("Salt", "Sugar", "Flour", "Eggs", "Milk")
|
||||
.map(name -> {
|
||||
return new Ingredient(name, 1.0, 2.0, 3.0);
|
||||
})
|
||||
.forEach(ingredientRepository::save);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
controller = new IngredientController(ingredientRepository, template);
|
||||
|
||||
this.createInitialIngredients();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAllIngredients() {
|
||||
List<Ingredient> ingredients = controller.getIngredients(Optional.empty(), Optional.empty()).getBody();
|
||||
|
||||
assertNotNull(ingredients);
|
||||
assertEquals(5, ingredients.size());
|
||||
assertEquals("Eggs", ingredients.getFirst().name);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPaginatedIngredients() {
|
||||
List<Ingredient> ingredients = controller.getIngredients(Optional.of(0), Optional.of(2)).getBody();
|
||||
|
||||
assertNotNull(ingredients);
|
||||
assertEquals(2, ingredients.size());
|
||||
assertEquals("Eggs", ingredients.get(0).name);
|
||||
assertEquals("Flour", ingredients.get(1).name);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetIngredientById() {
|
||||
Ingredient ingredient = ingredientRepository.findAll().getFirst();
|
||||
Long id = ingredient.id;
|
||||
|
||||
Ingredient fetchedIngredient = controller.getIngredientById(id).getBody();
|
||||
|
||||
assertNotNull(fetchedIngredient);
|
||||
assertEquals(ingredient.name, fetchedIngredient.name);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetIngredientByInvalidId() {
|
||||
Long invalidId = 2137L;
|
||||
|
||||
var response = controller.getIngredientById(invalidId);
|
||||
|
||||
assertEquals(HttpStatusCode.valueOf(404), response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateIngredient() {
|
||||
Ingredient newIngredient = new Ingredient("Butter", 0.5, 1.0, 1.5);
|
||||
|
||||
Ingredient createdIngredient = controller.createIngredient(newIngredient).getBody();
|
||||
|
||||
assertNotNull(createdIngredient);
|
||||
assertEquals("Butter", createdIngredient.name);
|
||||
assertEquals(6, ingredientRepository.count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateIngredientMissingName() {
|
||||
Ingredient newIngredient = new Ingredient(null, 0.5, 1.0, 1.5);
|
||||
|
||||
var response = controller.createIngredient(newIngredient);
|
||||
|
||||
assertEquals(HttpStatusCode.valueOf(400), response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateIngredient() {
|
||||
Ingredient ingredient = ingredientRepository.findAll()
|
||||
.stream()
|
||||
.filter(i -> i.name.equals("Salt"))
|
||||
.findFirst()
|
||||
.get(); // Should exist, no need for a check
|
||||
|
||||
Long id = ingredient.id;
|
||||
|
||||
Ingredient updatedData = new Ingredient("Sea Salt", 1.5, 2.5, 3.5);
|
||||
|
||||
Ingredient updatedIngredient = controller.updateIngredient(id, updatedData).getBody();
|
||||
|
||||
assertNotNull(updatedIngredient);
|
||||
assertEquals("Sea Salt", updatedIngredient.name);
|
||||
assertEquals(1.5, updatedIngredient.proteinPer100g);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateMissingIngredient() {
|
||||
Long invalidId = 2137L;
|
||||
|
||||
Ingredient updatedData = new Ingredient("Sea Salt", 1.5, 2.5, 3.5);
|
||||
|
||||
var response = controller.updateIngredient(invalidId, updatedData);
|
||||
|
||||
assertEquals(HttpStatusCode.valueOf(404), response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteIngredient() {
|
||||
Ingredient ingredient = ingredientRepository.findAll()
|
||||
.stream()
|
||||
.filter(i -> i.name.equals("Sugar"))
|
||||
.findFirst()
|
||||
.get(); // Should exist, no need for a check
|
||||
|
||||
Long id = ingredient.id;
|
||||
|
||||
var response = controller.deleteIngredient(id);
|
||||
|
||||
assertEquals(HttpStatusCode.valueOf(200), response.getStatusCode());
|
||||
assertEquals(4, ingredientRepository.count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteMissingIngredient() {
|
||||
Long invalidId = 2137L;
|
||||
|
||||
var response = controller.deleteIngredient(invalidId);
|
||||
|
||||
assertEquals(HttpStatusCode.valueOf(404), response.getStatusCode());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue