Skip to content
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.codedifferently.lesson16.web;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class CreatePatronResponse {
private PatronResponse patron;
}
Original file line number Diff line number Diff line change
@@ -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<PatronResponse> patrons;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<MediaItem> items = library.search(SearchCriteria.builder().id(id.toString()).build());
if (items.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Media item not found");
}
List<MediaItemResponse> responseItems = items.stream().map(MediaItemResponse::from).toList();
var response = GetMediaItemsResponse.builder().items(responseItems).build();
return response;
}

@DeleteMapping("/items/{id}")
public ResponseEntity<Void> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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;

@RestController
public class PatronsController {
private final Library library;

public PatronsController(Library library) throws IOException {
this.library = library;
}

@GetMapping("/patrons")
public GetPatronsResponse getPatrons() {
Set<LibraryGuest> patrons = library.getPatrons();
List<PatronResponse> 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<LibraryGuest> patrons = new HashSet<>();
for (LibraryGuest guest : library.getPatrons()) {
if (guest.getId() == id) {
patrons.add(guest);
}
}
List<PatronResponse> responsePatrons = patrons.stream().map(PatronResponse::from).toList();
var response = GetPatronsResponse.builder().patrons(responsePatrons).build();
return response;
}

@DeleteMapping("/patrons/{id}")
public ResponseEntity<Void> 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
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;

@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<LibraryGuest> 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": "[email protected]"
}
}
""";

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<LibraryGuest> 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<LibraryGuest> list) {
for (LibraryGuest guest : list) {
if (guest.getCheckedOutMediaItems().size() == 0) {
return guest.getId();
}
}
return list.get(4).getId();
}
}