Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add subscription data to book details #31

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.productdock.library.gateway.book;

import java.util.List;

public record BookDetailsDto(Object bookData, List<Object> rentalRecords, Integer availableBookCount,
Boolean bookSubscription) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@
public class BookDetailsResponseCombiner {

private static final String JSON_FIELD_RECORDS = "records";
private static final String JSON_FIELD_SUBSCRIPTION = "subscribed";

public JsonNode generateBookDetailsDto(Object book, List<Object> rentalRecords, int availableBooksCount) {
List<Object> records = combineRentalRecordsWithAvailable(rentalRecords, availableBooksCount);
var json = jsonOf(book);
public JsonNode generateBookDetailsDto(BookDetailsDto bookDetailsDto) {
List<Object> records = combineRentalRecordsWithAvailable(bookDetailsDto.rentalRecords(), bookDetailsDto.availableBookCount());
var json = jsonOf(bookDetailsDto.bookData());
extendJsonWithRecords((ObjectNode) json, jsonOf(records));
extendJsonWithSubscription((ObjectNode) json, jsonOf(bookDetailsDto.bookSubscription()));

return json;
}

Expand All @@ -38,4 +41,8 @@ private List<AvailableRentalRecordDto> generateAvailableRecords(int availableBoo
private void extendJsonWithRecords(ObjectNode json, JsonNode records) {
json.putIfAbsent(JSON_FIELD_RECORDS, records);
}

private void extendJsonWithSubscription(ObjectNode json, JsonNode bookSubscription) {
json.putIfAbsent(JSON_FIELD_SUBSCRIPTION, bookSubscription);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,60 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jwt.JWTParser;
import com.productdock.library.gateway.client.CatalogClient;
import com.productdock.library.gateway.client.InventoryClient;
import com.productdock.library.gateway.client.RentalClient;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple4;

import java.text.ParseException;
import java.util.List;

@Service
public record BookService(CatalogClient catalogClient, RentalClient rentalClient,
InventoryClient inventoryClient, BookDetailsResponseCombiner bookDetailsResponseCombiner) {

private static final String CLAIM_EMAIL = "email";

@SneakyThrows
public JsonNode getBookDetailsById(String bookId, String jwtToken) {
var userId = getClaimEmail(jwtToken);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename getClaimEmail toextractUserId or similar, because userId is what you get


var bookDtoMono = catalogClient.getBookData(bookId, jwtToken);
var rentalRecordsDtoMono = rentalClient.getBookRentalRecords(bookId, jwtToken);
var availableBooksCountMono = inventoryClient.getAvailableBookCopiesCount(bookId, jwtToken);
var bookSubscriptionMono = inventoryClient.isUserSubscribedToBook(bookId, jwtToken, userId);

var bookDetailsDtoMono = generateBookDetailsDtoMono(Mono.zip(bookDtoMono, rentalRecordsDtoMono, availableBooksCountMono, bookSubscriptionMono));

var bookDetailsDtoMono = Mono.zip(bookDtoMono, rentalRecordsDtoMono, availableBooksCountMono).flatMap(tuple -> {
var book = bookDetailsResponseCombiner.generateBookDetailsDto(tuple.getT1(), tuple.getT2(), tuple.getT3());
return Mono.just(book);
});
return bookDetailsDtoMono.toFuture().get();
}

@SneakyThrows
public JsonNode getBookDetailsByTitleAndAuthor(String title, String author, String jwtToken) {
var userId = getClaimEmail(jwtToken);

var bookDtoMono = catalogClient.getBookDataByTitleAndAuthor(title, author, jwtToken);
var bookDetails = bookDtoMono.toFuture().get();
String bookId = getIdFromBook(bookDetails);
var rentalRecordsDtoMono = rentalClient.getBookRentalRecords(bookId, jwtToken);
var availableBooksCountMono = inventoryClient.getAvailableBookCopiesCount(bookId, jwtToken);
var bookSubscriptionMono = inventoryClient.isUserSubscribedToBook(bookId, jwtToken, userId);

var bookDetailsDtoMono = Mono.zip(rentalRecordsDtoMono, availableBooksCountMono).flatMap(tuple -> {
var book = bookDetailsResponseCombiner.generateBookDetailsDto(bookDetails, tuple.getT1(), tuple.getT2());
var bookDetailsDtoMono = generateBookDetailsDtoMono(Mono.zip(Mono.just(bookDetails), rentalRecordsDtoMono, availableBooksCountMono, bookSubscriptionMono));

return bookDetailsDtoMono.toFuture().get();
}

private Mono<JsonNode> generateBookDetailsDtoMono(Mono<Tuple4<Object, List<Object>, Integer, Boolean>> mono) {
return mono.flatMap(tuple -> {
var bookDetailsDto = new BookDetailsDto(tuple.getT1(), tuple.getT2(), tuple.getT3(), tuple.getT4());
var book = bookDetailsResponseCombiner.generateBookDetailsDto(bookDetailsDto);
return Mono.just(book);
});
return bookDetailsDtoMono.toFuture().get();
}

private String getIdFromBook(Object bookDetails) {
Expand All @@ -47,6 +64,11 @@ private String getIdFromBook(Object bookDetails) {
return bookIdNode.asText();
}

private String getClaimEmail(String jwtToken) throws ParseException {
var jwt = JWTParser.parse(jwtToken);
return jwt.getJWTClaimsSet().getClaim(CLAIM_EMAIL).toString();
}

private JsonNode jsonOf(Object book) {
return new ObjectMapper().valueToTree(book);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.productdock.library.gateway.book;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.Date;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class BookSubscriptionDto {
private String bookId;
private String userId;
private Date createdDate;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this DTO?

Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,49 @@
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.DefaultUriBuilderFactory;
import reactor.core.publisher.Mono;

@Component
public class InventoryClient {

@Value("${inventory.service.url}/api/inventory/book/")
@Value("${inventory.service.url}")
private String inventoryServiceUrl;

private WebClient webClient;

public InventoryClient(){
public InventoryClient() {
this.webClient = WebClient.create();
}

public Mono<Integer> getAvailableBookCopiesCount(String bookId, String jwtToken){
var inventoryBookUrl = inventoryServiceUrl + bookId;
public Mono<Integer> getAvailableBookCopiesCount(String bookId, String jwtToken) {
var inventoryBookUrl = inventoryServiceUrl + "/api/inventory/books/" + bookId;

return webClient
.get()
.uri(inventoryBookUrl)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + jwtToken)
.retrieve()
.bodyToMono(Integer.class)
.onErrorReturn(RuntimeException.class,0);
.onErrorReturn(RuntimeException.class, 0);
}

public Mono<Boolean> isUserSubscribedToBook(String bookId, String jwtToken, String userId) {
var subscriptionUrl = inventoryServiceUrl + "/api/inventory/books/" + bookId + "/subscriptions";
var uri = new DefaultUriBuilderFactory(subscriptionUrl)
.builder()
.queryParam("userId", userId)
.build();


return webClient
.get()
.uri(uri)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + jwtToken)
.retrieve()
.toBodilessEntity()
.map(responseEntity -> responseEntity.getStatusCode().is2xxSuccessful())
.onErrorReturn(false);
}

}
4 changes: 4 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ spring:
uri: ${USER_PROFILES_SERVICE_URL}
predicates:
- Path=/api/user-profiles/**
- id: inventory-route
uri: ${INVENTORY_SERVICE_URL}
predicates:
- Path=/api/inventory/**

data:
mongodb:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import org.springframework.test.web.reactive.server.WebTestClient;

import java.io.IOException;
import java.util.Base64;
import java.util.Date;

import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize;
Expand Down Expand Up @@ -54,7 +56,8 @@ static void tearDown() throws IOException {
@Test
@WithMockUser
void givenBookId_thenGetBookDetails() {
mockUserProfilesBackEnd.enqueue(new MockResponse().setBody("id-token"));
var token = generateToken();
mockUserProfilesBackEnd.enqueue(new MockResponse().setBody(token));
mockCatalogBackEnd.enqueue(new MockResponse()
.setBody("{\"id\": \"1\", \"title\": \"Title\", \"author\": \"John Doe\", \"cover\": \"Cover\", " +
"\"reviews\":[{\"userFullName\":\"John Doe\",\"rating\":5,\"recommendation\":[\"JUNIOR\"],\"comment\":\"Must read!\"}]}")
Expand All @@ -73,13 +76,15 @@ void givenBookId_thenGetBookDetails() {
.jsonPath("$.reviews[0].recommendation[0]").isEqualTo("JUNIOR")
.jsonPath("$.reviews[0].recommendation").value(hasSize(1))
.jsonPath("$.reviews[0].comment").isEqualTo("Must read!")
.jsonPath("$.records").value(empty());
.jsonPath("$.records").value(empty())
.jsonPath("$.subscribed").isEqualTo(false);
}

@Test
@WithMockUser
void givenTitleAndAuthor_thenGetBookDetails() {
mockUserProfilesBackEnd.enqueue(new MockResponse().setBody("id-token"));
var token = generateToken();
mockUserProfilesBackEnd.enqueue(new MockResponse().setBody(token));
mockCatalogBackEnd.enqueue(new MockResponse()
.setBody("{\"id\": \"1\", \"title\": \"Title\", \"author\": \"John Doe\", \"cover\": \"Cover\", " +
"\"reviews\":[{\"userFullName\":\"John Doe\",\"rating\":5,\"recommendation\":[\"JUNIOR\"],\"comment\":\"Must read!\"}]}")
Expand All @@ -98,6 +103,16 @@ void givenTitleAndAuthor_thenGetBookDetails() {
.jsonPath("$.reviews[0].recommendation[0]").isEqualTo("JUNIOR")
.jsonPath("$.reviews[0].recommendation").value(hasSize(1))
.jsonPath("$.reviews[0].comment").isEqualTo("Must read!")
.jsonPath("$.records").value(empty());
.jsonPath("$.records").value(empty())
.jsonPath("$.subscribed").isEqualTo(false);
}

private String generateToken() {
var email = "user@email.com";
var header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
var payload = "{\"sub\":\"subject\",\"email\":\"" + email + "\",\"iat\":" + new Date().getTime() + "}";
var headerBase64 = Base64.getEncoder().encodeToString(header.getBytes());
var payloadBase64 = Base64.getEncoder().encodeToString(payload.getBytes());
return headerBase64 + "." + payloadBase64 + ".signature";
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.productdock.library.gateway.service;

import com.productdock.library.gateway.book.BookDetailsDto;
import com.productdock.library.gateway.book.BookDetailsResponseCombiner;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
Expand All @@ -25,8 +26,10 @@ void generateBookDetailsDtoWithAvailableRecord_whenAvailableBookCopiesExist() {
anyRecord.put("recordProperty", "recordValue");
List<Object> rentalRecordsDto = List.of(anyRecord);
var availableBookCount = 1;
var bookSubscription = false;
var bookDetailsDto = new BookDetailsDto(anyDto, rentalRecordsDto, availableBookCount, bookSubscription);

var bookDetails = bookDetailsResponseCombiner.generateBookDetailsDto(anyDto, rentalRecordsDto, availableBookCount);
var bookDetails = bookDetailsResponseCombiner.generateBookDetailsDto(bookDetailsDto);

assertThat(bookDetails.get("property").asText()).isEqualTo("value");
assertThat(bookDetails.get("records")).isNotNull();
Expand All @@ -43,8 +46,10 @@ void generateBookDetailsDto_whenAvailableBookCopiesDoNotExist() {
anyRecord.put("recordProperty", "recordValue");
List<Object> rentalRecordsDto = List.of(anyRecord);
var availableBookCount = 0;

var bookDetails = bookDetailsResponseCombiner.generateBookDetailsDto(anyDto, rentalRecordsDto, availableBookCount);
var bookSubscription = false;
var bookDetailsDto = new BookDetailsDto(anyDto, rentalRecordsDto, availableBookCount, bookSubscription);

var bookDetails = bookDetailsResponseCombiner.generateBookDetailsDto(bookDetailsDto);

assertThat(bookDetails.get("records")).isNotNull();
assertThat(bookDetails.get("records").size()).isEqualTo(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;

Expand All @@ -28,13 +29,16 @@ class BookServiceShould {
private static final String BOOK_ID = "1";
private static final String BOOK_TITLE = "::title::";
private static final String BOOK_AUTHOR = "::author::";
private static final String JWT_TOKEN = "";
private static final String JWT_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImVtYWlsIn0.9_82fPfiHdHoOkyx8WQY8FsgPivtguil3QL5a3bDE7g";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be avoided to store any secrets in Git, even if it's used for testing.
Is it possible to create an Util class that will generate for you a token which can be used for the test?
Please investigate and let me know

private static final String USER_ID = "email";
private static final int AVAILABLE_BOOK_COUNT = 1;
private static final boolean BOOK_SUBSCRIPTION = false;
private static final Object CATALOG_RESPONSE = Mockito.mock(Object.class);
private static final List<Object> RENTAL_RESPONSE = List.of(mock(Object.class));
private static final Mono<Object> CATALOG_MONO = Mono.just(CATALOG_RESPONSE);
private static final Mono<List<Object>> RENTAL_MONO = Mono.just(RENTAL_RESPONSE);
private static final Mono<Integer> AVAILABLE_BOOK_COUNT_MONO = Mono.just(AVAILABLE_BOOK_COUNT);
private static final Mono<Boolean> BOOK_SUBSCRIPTION_MONO = Mono.just(BOOK_SUBSCRIPTION);
private static final JsonNode BOOK_DETAILS_JSON = Mockito.mock(JsonNode.class);

@InjectMocks
Expand All @@ -57,7 +61,8 @@ void generateBookDetailsDtoByBookId() {
given(catalogClient.getBookData(BOOK_ID, JWT_TOKEN)).willReturn(CATALOG_MONO);
given(rentalClient.getBookRentalRecords(BOOK_ID, JWT_TOKEN)).willReturn(RENTAL_MONO);
given(inventoryClient.getAvailableBookCopiesCount(BOOK_ID, JWT_TOKEN)).willReturn(AVAILABLE_BOOK_COUNT_MONO);
given(bookDetailsResponseCombiner.generateBookDetailsDto(CATALOG_RESPONSE, RENTAL_RESPONSE, AVAILABLE_BOOK_COUNT)).willReturn(BOOK_DETAILS_JSON);
given(inventoryClient.isUserSubscribedToBook(BOOK_ID, JWT_TOKEN, USER_ID)).willReturn(BOOK_SUBSCRIPTION_MONO);
given(bookDetailsResponseCombiner.generateBookDetailsDto(any())).willReturn(BOOK_DETAILS_JSON);

var bookDetails = bookService.getBookDetailsById(BOOK_ID, JWT_TOKEN);

Expand All @@ -73,7 +78,8 @@ void generateBookDetailsDtoByBookTitleAndAuthor() {
given(catalogClient.getBookDataByTitleAndAuthor(BOOK_TITLE, BOOK_AUTHOR, JWT_TOKEN)).willReturn(catalogMono);
given(rentalClient.getBookRentalRecords(BOOK_ID, JWT_TOKEN)).willReturn(RENTAL_MONO);
given(inventoryClient.getAvailableBookCopiesCount(BOOK_ID, JWT_TOKEN)).willReturn(AVAILABLE_BOOK_COUNT_MONO);
given(bookDetailsResponseCombiner.generateBookDetailsDto(catalogResponse, RENTAL_RESPONSE, AVAILABLE_BOOK_COUNT))
given(inventoryClient.isUserSubscribedToBook(BOOK_ID, JWT_TOKEN, USER_ID)).willReturn(BOOK_SUBSCRIPTION_MONO);
given(bookDetailsResponseCombiner.generateBookDetailsDto(any()))
.willReturn(BOOK_DETAILS_JSON);

var bookDetails = bookService.getBookDetailsByTitleAndAuthor(BOOK_TITLE, BOOK_AUTHOR, JWT_TOKEN);
Expand Down
Loading