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..3799bed1 --- /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 PatronResponse patron; +} 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..846639a5 --- /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 patrons; +} 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 d4bfb7bf..0f4536ce 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,12 +4,25 @@ 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; +import java.util.UUID; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; 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; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +// ___________________________________________________________ +// THIS CODE WAS MADE IN COLLABORATION WITH VICENTE AND RICH +// ___________________________________________________________ @RestController @CrossOrigin @@ -29,4 +42,37 @@ public GetMediaItemsResponse getItems() { var response = GetMediaItemsResponse.builder().items(responseItems).build(); return response; } + + /** + * Post an item to the specified endpoint. + * + * @param req the request object for creating a media item + * @return the response object for creating a media item + */ + @PostMapping("/items") + public CreateMediaItemResponse postItem(@Valid @RequestBody CreateMediaItemRequest req) { + MediaItem media = MediaItemRequest.asMediaItem(req.getItem()); + library.addMediaItem(media, librarian); + return CreateMediaItemResponse.builder().item(MediaItemResponse.from(media)).build(); + } + + @GetMapping("/items/{id}") + public GetMediaItemsResponse getItem(@PathVariable UUID id) { + Set items = library.search(SearchCriteria.builder().id(id.toString()).build()); + if (items.isEmpty()) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Media item not found"); + } + List responseItems = items.stream().map(MediaItemResponse::from).toList(); + var response = GetMediaItemsResponse.builder().items(responseItems).build(); + return response; + } + + @DeleteMapping("/items/{id}") + public ResponseEntity deleteItem(@PathVariable UUID id) { + if (!library.hasMediaItem(id)) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Media item not found"); + } + library.removeMediaItem(id, librarian); + return ResponseEntity.noContent().build(); + } } 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..c0f24788 --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronRequest.java @@ -0,0 +1,27 @@ +package com.codedifferently.lesson16.web; + +import com.codedifferently.lesson16.library.Patron; +import jakarta.validation.constraints.NotBlank; +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; + + @NotBlank(message = "Email is required") + private String email; + + @NotBlank(message = "Name is required") + private String name; + + public static Patron asPatron(PatronRequest request) { + return new Patron(request.name, request.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..2e00587b --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronResponse.java @@ -0,0 +1,25 @@ +package com.codedifferently.lesson16.web; + +import com.codedifferently.lesson16.library.LibraryGuest; +import jakarta.validation.constraints.NotBlank; +import java.util.UUID; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class PatronResponse { + private UUID id; + + @NotBlank(message = "Email is required") + private String email; + + @NotBlank(message = "Name is required") + private String name; + + public static PatronResponse from(LibraryGuest 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..7fb52e91 --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronsController.java @@ -0,0 +1,73 @@ +package com.codedifferently.lesson16.web; + +import com.codedifferently.lesson16.library.Library; +import com.codedifferently.lesson16.library.LibraryGuest; +import com.codedifferently.lesson16.library.Patron; +import jakarta.validation.Valid; +import java.io.IOException; +import java.util.HashSet; +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.RestController; +import org.springframework.web.server.ResponseStatusException; + +// ___________________________________________________________ +// THIS CODE WAS MADE IN COLLABORATION WITH VICENTE AND RICH +// ___________________________________________________________ + +@RestController +public class PatronsController { + private final Library library; + + public PatronsController(Library library) throws IOException { + this.library = library; + } + + @GetMapping("/patrons") + public GetPatronsResponse getPatrons() { + Set patrons = library.getPatrons(); + List responsePatrons = patrons.stream().map(PatronResponse::from).toList(); + var response = GetPatronsResponse.builder().patrons(responsePatrons).build(); + return response; + } + + @PostMapping("/patrons") + public CreatePatronResponse postPatron(@Valid @RequestBody CreatePatronRequest req) { + Patron guest = PatronRequest.asPatron(req.getPatron()); + library.addLibraryGuest(guest); + return CreatePatronResponse.builder().patron(PatronResponse.from(guest)).build(); + } + + @GetMapping("/patrons/{id}") + public GetPatronsResponse getPatron(@PathVariable("id") UUID id) { + if (!library.hasLibraryGuest(id)) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Guest patron not found"); + } + Set patrons = new HashSet<>(); + for (LibraryGuest guest : library.getPatrons()) { + if (guest.getId() == id) { + patrons.add(guest); + } + } + List responsePatrons = patrons.stream().map(PatronResponse::from).toList(); + var response = GetPatronsResponse.builder().patrons(responsePatrons).build(); + return response; + } + + @DeleteMapping("/patrons/{id}") + public ResponseEntity deletePatron(@PathVariable() UUID id) { + if (!library.hasLibraryGuest(id)) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Guest patron not found"); + } + library.removeLibraryGuest(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/lesson_16/api/api_app/src/test/java/com/codedifferently/lesson16/web/PatronControllerTest.java b/lesson_16/api/api_app/src/test/java/com/codedifferently/lesson16/web/PatronControllerTest.java new file mode 100644 index 00000000..ac039c19 --- /dev/null +++ b/lesson_16/api/api_app/src/test/java/com/codedifferently/lesson16/web/PatronControllerTest.java @@ -0,0 +1,136 @@ +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.Library; +import com.codedifferently.lesson16.library.LibraryGuest; +import java.util.List; +import java.util.UUID; +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; + +// ___________________________________________________________ +// THIS CODE WAS MADE IN COLLABORATION WITH VICENTE AND RICH +// ___________________________________________________________ + +@SpringBootTest +@ContextConfiguration(classes = Lesson16.class) +class PatronControllerTest { + private static MockMvc mockMvc; + @Autowired private Library library; + + private Library lib = library; + + @BeforeAll + static void setUp(WebApplicationContext wac) { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + @Test + void testController_getsAnPatron() throws Exception { + List pat = library.getPatrons().stream().toList(); + UUID ids = pat.get(3).getId(); + + mockMvc + .perform(get("/patrons/" + ids.toString()).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + void testController_getsAllPatrons() throws Exception { + mockMvc + .perform(get("/patrons").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.patrons").isArray()) + .andExpect(jsonPath("$.patrons.length()").value(5)); + } + + @Test + void testController_returnsNotFoundOnGetPatron() throws Exception { + mockMvc + .perform( + get("/patrons/00000000-0000-0000-0000-000000000000") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + void testController_reportsBadRequestOnAddPatron() throws Exception { + String json = "{}"; + + mockMvc + .perform(post("/patrons").contentType(MediaType.APPLICATION_JSON).content(json)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors").isArray()) + .andExpect(jsonPath("$.errors.length()").value(1)); + } + + @Test + void testController_addsPatron() throws Exception { + String json = + """ + { + "patron":{ + "name": "John Book", + "email": "johk@reallibrary.org" + } + } + """; + + mockMvc + .perform(post("/patrons").contentType(MediaType.APPLICATION_JSON).content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.patron.name").value("John Book")); + } + + @Test + void testController_returnsNotFoundOnDeletePatron() throws Exception { + mockMvc + .perform( + delete("/patrons/00000000-0000-0000-0000-000000000000") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + void testController_deletesPatron() throws Exception { + Library lib = library; + List pat = library.getPatrons().stream().toList(); + UUID ids = getGuestId(pat); + + mockMvc + .perform(delete("/patrons/" + ids.toString()).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + int i = 0; + pat = library.getPatrons().stream().toList(); + for (LibraryGuest guest : pat) { + if (guest.getId() == ids) { + i++; + } + } + library = lib; + assertThat(i).isEqualTo(0); + } + + UUID getGuestId(List list) { + for (LibraryGuest guest : list) { + if (guest.getCheckedOutMediaItems().size() == 0) { + return guest.getId(); + } + } + return list.get(4).getId(); + } +}