From 83d6ecab7b2dc489ccb410989c9c2e5e7e615daa Mon Sep 17 00:00:00 2001 From: SherllinT Date: Mon, 8 Apr 2024 15:50:39 +0000 Subject: [PATCH 1/3] feat: MediaItemsController, missing http status request --- .../lesson16/web/MediaItemsController.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java index cae5ff06..cc8a4ac8 100644 --- a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java @@ -7,7 +7,15 @@ import java.io.IOException; import java.util.List; import java.util.Set; +import java.util.UUID; +import org.springframework.http.HttpStatus; +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.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController @@ -27,4 +35,32 @@ public GetMediaItemsResponse getItems() { var response = GetMediaItemsResponse.builder().items(responseItems).build(); return response; } + + @GetMapping("/items/{id}") + public GetMediaItemsResponse getItem() { + Set items = library.search(SearchCriteria.builder().build()); + List responseItems = items.stream().map(MediaItemResponse::from).toList(); + var response = GetMediaItemsResponse.builder().items(responseItems).build(); + return response; + } + + @DeleteMapping("/items/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public ResponseEntity deleteItem(@PathVariable("id") UUID id) { + if (!library.hasMediaItem(id)) { + return ResponseEntity.notFound().build(); + } + library.removeMediaItem(id, librarian); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/items") + @ResponseStatus(HttpStatus.BAD_REQUEST) + public CreateMediaItemResponse createItem(@RequestBody CreateMediaItemRequest request) { + MediaItem item = MediaItemRequest.asMediaItem(request.getItem()); + library.addMediaItem(item, librarian); + var response = CreateMediaItemResponse.builder().item(MediaItemResponse.from(item)).build(); + + return response; + } } From e3a8f7ffcaf2148f4cc55645ec21593ad09eedaa Mon Sep 17 00:00:00 2001 From: SherllinT Date: Tue, 9 Apr 2024 14:35:24 +0000 Subject: [PATCH 2/3] feat: updated http status in lesson 16 --- .../lesson16/web/MediaItemsController.java | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java index cc8a4ac8..d8a5cd2f 100644 --- a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java @@ -4,6 +4,7 @@ import com.codedifferently.lesson16.library.Library; import com.codedifferently.lesson16.library.MediaItem; import com.codedifferently.lesson16.library.search.SearchCriteria; +import jakarta.validation.Valid; import java.io.IOException; import java.util.List; import java.util.Set; @@ -28,20 +29,30 @@ public MediaItemsController(Library library) throws IOException { this.librarian = library.getLibrarians().stream().findFirst().orElseThrow(); } - @GetMapping("/items") - public GetMediaItemsResponse getItems() { + @GetMapping("/items/{id}") + public ResponseEntity getItem(@PathVariable("id") UUID id) { + if (!library.hasMediaItem(id)) { + return ResponseEntity.notFound().build(); // Return 404 Not Found + } Set items = library.search(SearchCriteria.builder().build()); List responseItems = items.stream().map(MediaItemResponse::from).toList(); var response = GetMediaItemsResponse.builder().items(responseItems).build(); - return response; + return ResponseEntity.ok(response); } - @GetMapping("/items/{id}") - public GetMediaItemsResponse getItem() { - Set items = library.search(SearchCriteria.builder().build()); - List responseItems = items.stream().map(MediaItemResponse::from).toList(); - var response = GetMediaItemsResponse.builder().items(responseItems).build(); - return response; + @GetMapping("/items") + public ResponseEntity getItems() { + try { + Set items = library.search(SearchCriteria.builder().build()); + List responseItems = items.stream().map(MediaItemResponse::from).toList(); + var response = GetMediaItemsResponse.builder().items(responseItems).build(); + return ResponseEntity.ok(response); + } catch (Exception ex) { + // Log the exception for debugging purposes + ex.printStackTrace(); + // Return an appropriate error response + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } } @DeleteMapping("/items/{id}") @@ -55,8 +66,7 @@ public ResponseEntity deleteItem(@PathVariable("id") UUID id) { } @PostMapping("/items") - @ResponseStatus(HttpStatus.BAD_REQUEST) - public CreateMediaItemResponse createItem(@RequestBody CreateMediaItemRequest request) { + public CreateMediaItemResponse createItem(@Valid @RequestBody CreateMediaItemRequest request) { MediaItem item = MediaItemRequest.asMediaItem(request.getItem()); library.addMediaItem(item, librarian); var response = CreateMediaItemResponse.builder().item(MediaItemResponse.from(item)).build(); From 489cc2fe407e62c4d730205d4ba3a50b3e0ca751 Mon Sep 17 00:00:00 2001 From: SherllinT Date: Thu, 11 Apr 2024 17:45:01 +0000 Subject: [PATCH 3/3] feat: Patron files, only missing test --- .../lesson16/web/CreatePatronRequest.java | 17 +++ .../lesson16/web/CreatePatronResponse.java | 10 ++ .../lesson16/web/GetPatronsResponse.java | 12 ++ .../lesson16/web/MediaItemsController.java | 2 +- .../lesson16/web/PatronRequest.java | 17 +++ .../lesson16/web/PatronResponse.java | 22 +++ .../lesson16/web/PatronsController.java | 64 +++++++++ .../lesson16/web/PatronsControllerTest.java | 125 ++++++++++++++++++ 8 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/CreatePatronRequest.java create mode 100644 lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/CreatePatronResponse.java create mode 100644 lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/GetPatronsResponse.java create mode 100644 lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronRequest.java create mode 100644 lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronResponse.java create mode 100644 lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronsController.java create mode 100644 lesson_16/api/api_app/src/test/java/com/codedifferently/lesson16/web/PatronsControllerTest.java diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/CreatePatronRequest.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/CreatePatronRequest.java new file mode 100644 index 00000000..db557148 --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/CreatePatronRequest.java @@ -0,0 +1,17 @@ +package com.codedifferently.lesson16.web; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CreatePatronRequest { + @NotNull(message = "patron is required") @Valid + private PatronRequest patron; +} diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/CreatePatronResponse.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/CreatePatronResponse.java new file mode 100644 index 00000000..5366e2f5 --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/CreatePatronResponse.java @@ -0,0 +1,10 @@ +package com.codedifferently.lesson16.web; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class CreatePatronResponse { + private MediaItemResponse item; +} diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/GetPatronsResponse.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/GetPatronsResponse.java new file mode 100644 index 00000000..5540a6ac --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/GetPatronsResponse.java @@ -0,0 +1,12 @@ +package com.codedifferently.lesson16.web; + +import java.util.List; +import lombok.Builder; +import lombok.Data; +import lombok.Singular; + +@Data +@Builder +public class GetPatronsResponse { + @Singular private List patron; +} diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java index 42c30754..39062008 100644 --- a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java @@ -11,8 +11,8 @@ import java.util.UUID; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.CrossOrigin; +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; diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronRequest.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronRequest.java new file mode 100644 index 00000000..6601a47c --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronRequest.java @@ -0,0 +1,17 @@ +package com.codedifferently.lesson16.web; + +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PatronRequest { + private UUID id; + private String name; + private String email; +} diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronResponse.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronResponse.java new file mode 100644 index 00000000..464f887e --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronResponse.java @@ -0,0 +1,22 @@ +package com.codedifferently.lesson16.web; + +import com.codedifferently.lesson16.library.Patron; +import java.util.UUID; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class PatronResponse { + + private UUID id; + private String name; + private String email; + + public static PatronResponse from(Patron patron) { + var result = + PatronResponse.builder().id(patron.getId()).name(patron.getName()).email(patron.getEmail()); + + return result.build(); + } +} diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronsController.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronsController.java new file mode 100644 index 00000000..647c86d2 --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronsController.java @@ -0,0 +1,64 @@ +package com.codedifferently.lesson16.web; + +import com.codedifferently.lesson16.library.Library; +import com.codedifferently.lesson16.library.Patron; +import com.codedifferently.lesson16.library.search.SearchCriteria; +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import main.java.com.codedifferently.lesson16.web.CreatePatronResponse; +import org.springframework.http.HttpStatus; +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.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class PatronsController { + private final Library library; + + public PatronsController(Library library) throws IOException { + this.library = library; + } + + @GetMapping("/patrons") + public ResponseEntity> getPatrons(){ + Set patronsSet = library.getAllPatrons(); + List patronsList = new ArrayList<>(patronsSet); + return new ResponseEntity<>(patronsList, HttpStatus.OK); + } + + @GetMapping("/patron/{id}") + public ResponseEntity getPatron(@PathVariable("id") UUID id) { + Patron patron = library.getPatronById(id); + if (patron == null) { + return ResponseEntity.notFound().build(); + } + GetPatronsResponse response = GetPatronsResponse.from(patron); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/patron/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public ResponseEntity deletePatron(@PathVariable("id") UUID id) { + if (!library.hasPatron(id)) { + return ResponseEntity.notFound().build(); + } + library.removePatron(id); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/patron") + @ResponseStatus(HttpStatus.CREATED) + public CreatePatronResponse createPatron(@RequestBody CreatePatronRequest request) { + Patron patron = request.getPatron(); + library.addPatron(patron); + CreatePatronResponse response = CreatePatronResponse.from(patron); + return response; + } +} diff --git a/lesson_16/api/api_app/src/test/java/com/codedifferently/lesson16/web/PatronsControllerTest.java b/lesson_16/api/api_app/src/test/java/com/codedifferently/lesson16/web/PatronsControllerTest.java new file mode 100644 index 00000000..2b922a32 --- /dev/null +++ b/lesson_16/api/api_app/src/test/java/com/codedifferently/lesson16/web/PatronsControllerTest.java @@ -0,0 +1,125 @@ +package com.codedifferently.lesson16.web; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.codedifferently.lesson16.Lesson16; +import com.codedifferently.lesson16.library.Book; +import com.codedifferently.lesson16.library.Library; +import com.codedifferently.lesson16.library.MediaItem; +import com.codedifferently.lesson16.library.search.SearchCriteria; +import java.util.Set; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest +@ContextConfiguration(classes = Lesson16.class) +class PatronsControllerTest { + private static MockMvc mockMvc; + @Autowired private Library library; + + @BeforeAll + static void setUp(WebApplicationContext wac) { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + @Test + void testController_patronGetsAllItems() throws Exception { + mockMvc + .perform(get("/patrons").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.items").isArray()) + .andExpect(jsonPath("$.items.length()").value(31)); + } + + @Test + void testController_getsAnItem() throws Exception { + mockMvc + .perform( + get("/items/31616162-3831-3832-2d34-3334352d3465") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + void testController_returnsNotFoundOnGetItem() throws Exception { + mockMvc + .perform( + get("/items/00000000-0000-0000-0000-000000000000") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + void testController_reportsBadRequestOnAddItem() throws Exception { + String json = "{}"; + + mockMvc + .perform(post("/items").contentType(MediaType.APPLICATION_JSON).content(json)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors").isArray()) + .andExpect(jsonPath("$.errors.length()").value(1)); + } + + @Test + void testController_addsItem() throws Exception { + String json = + """ + { + "item": { + "id": "e27a4e0d-9664-420d-955e-c0e295d0ce02", + "type": "BOOK", + "title": "Becoming", + "isbn": "9781524763138", + "authors": ["Michelle Obama"], + "pages": 448 + } + } + """; + + mockMvc + .perform(post("/items").contentType(MediaType.APPLICATION_JSON).content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.item.id").value("e27a4e0d-9664-420d-955e-c0e295d0ce02")); + + Set items = + library.search(SearchCriteria.builder().id("e27a4e0d-9664-420d-955e-c0e295d0ce02").build()); + assertThat(items).hasSize(1); + var item = items.iterator().next(); + assertThat(item).isInstanceOf(Book.class); + assertThat(item.getTitle()).isEqualTo("Becoming"); + } + + @Test + void testController_returnsNotFoundOnDeleteItem() throws Exception { + mockMvc + .perform( + delete("/items/00000000-0000-0000-0000-000000000000") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + void testController_deletesItem() throws Exception { + mockMvc + .perform( + delete("/items/32623932-6566-3364-2d62-3232342d3435") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + Set items = + library.search(SearchCriteria.builder().id("32623932-6566-3364-2d62-3232342d3435").build()); + assertThat(items).hasSize(0); + } +}