Skip to content

Commit

Permalink
EDGEDCB-41 implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
AntonAntonich committed Feb 18, 2025
1 parent f0cf171 commit 108fd8e
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 54 deletions.
3 changes: 3 additions & 0 deletions src/main/java/org/folio/ed/client/DcbClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ TransactionStatusResponseCollection getTransactionStatusList(@RequestParam("from

@PutMapping(value = "/{dcbTransactionId}")
void updateTransactionDetails(@PathVariable("dcbTransactionId") String dcbTransactionId, @RequestBody DcbUpdateTransaction dcbUpdateTransaction);

@PutMapping(value = "/{dcbTransactionId}/renew")
TransactionStatusResponse renewLoanByTransactionId(@PathVariable String dcbTransactionId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ public class DcbTransactionController implements TransactionsApi {

private final DcbTransactionService dcbTransactionService;

@Override
public ResponseEntity<TransactionStatusResponse> renewItemLoanByTransactionId(String dcbTransactionId) {
return ResponseEntity.status(HttpStatus.OK)
.body(dcbTransactionService.renewLoanByTransactionId(dcbTransactionId));
}

@Override
public ResponseEntity<TransactionStatusResponse> getDCBTransactionStatusById(String dcbTransactionId) {
return ResponseEntity.status(HttpStatus.OK)
Expand Down Expand Up @@ -53,4 +59,6 @@ public ResponseEntity<Void> updateTransactionDetails(String dcbTransactionId, Dc
dcbTransactionService.updateTransactionDetails(dcbTransactionId, dcbUpdateTransaction);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}


}
4 changes: 4 additions & 0 deletions src/main/java/org/folio/ed/service/DcbTransactionService.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ public void updateTransactionDetails(String dcbTransactionId, DcbUpdateTransacti
dcbClient.updateTransactionDetails(dcbTransactionId, dcbUpdateTransaction);
}

public TransactionStatusResponse renewLoanByTransactionId(String dcbTransactionId) {
log.info("renewLoanByTransactionId:: Renewing loan for transaction id: {}", dcbTransactionId);
return dcbClient.renewLoanByTransactionId(dcbTransactionId);
}
}
17 changes: 17 additions & 0 deletions src/main/resources/swagger.api/edge-dcb.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,23 @@ paths:
$ref: '#/components/responses/UnprocessableEntity'
'500':
$ref: '#/components/responses/InternalServerError'
/transactions/{dcbTransactionId}/renew:
parameters:
- $ref: '#/components/parameters/dcbTransactionId'
put:
description: Increment the renew loan count and returned transaction status with renewal details
operationId: renewItemLoanByTransactionId
tags:
- circulation
responses:
'200':
$ref: '#/components/responses/TransactionStatusResponse'
'404':
$ref: '#/components/responses/NotFound'
'400':
$ref: '#/components/responses/BadRequest'
'500':
$ref: '#/components/responses/InternalServerError'
components:
requestBodies:
TransactionStatusBody:
Expand Down
175 changes: 123 additions & 52 deletions src/test/java/org/folio/ed/controller/DcbEdgeRequestHandlingTest.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
package org.folio.ed.controller;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.folio.ed.utils.EntityUtils.createDcbTransaction;
import static org.folio.ed.utils.EntityUtils.createDcbUpdateTransaction;
import static org.folio.ed.utils.EntityUtils.createTransactionStatus;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
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.request.MockMvcRequestBuilders.put;

import java.io.IOException;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.folio.ed.domain.dto.TransactionStatus;
import org.folio.edge.core.utils.ApiKeyUtils;
import org.folio.edgecommonspring.client.EdgeFeignClientProperties;
Expand All @@ -24,29 +35,25 @@
import org.springframework.http.MediaType;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import java.io.IOException;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;
import static org.folio.ed.utils.EntityUtils.createDcbTransaction;
import static org.folio.ed.utils.EntityUtils.createDcbUpdateTransaction;
import static org.folio.ed.utils.EntityUtils.createTransactionStatus;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.SneakyThrows;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class DcbEdgeRequestHandlingTest {

private final String TENANT = "test_tenant", USERNAME = "user", TOKEN = "This is totally a real test token!", TRANSACTION_ID = "123";
protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL)
private final String TENANT = "test_tenant";
private final String USERNAME = "user";
private final String TOKEN = "This is totally a real test token!";
private final String TRANSACTION_ID = "123";
protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().setSerializationInclusion(
JsonInclude.Include.NON_NULL)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);

Expand Down Expand Up @@ -83,8 +90,9 @@ void shouldConvertApiKeyToHeadersForGet() throws Exception {
.setResponseCode(200)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.setBody(responseBody));
var getResponse = mockMvc.perform(get("/dcbService/transactions/{transactionId}/status?apiKey={apiKey}", TRANSACTION_ID, apiKey)
.contentType(MediaType.APPLICATION_JSON))
var getResponse = mockMvc.perform(
get("/dcbService/transactions/{transactionId}/status?apiKey={apiKey}", TRANSACTION_ID, apiKey)
.contentType(MediaType.APPLICATION_JSON))
.andReturn()
.getResponse();
// Then the outgoing response from the edge API should contain the Okapi auth headers and the response body should
Expand All @@ -107,10 +115,11 @@ void shouldConvertApiKeyToHeadersForPost() throws Exception {
.setResponseCode(201)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.setBody(responseBody));
var postResponse = mockMvc.perform(post("/dcbService/transactions/{transactionId}?apiKey={apiKey}", TRANSACTION_ID, apiKey)
.content(asJsonString(createDcbTransaction()))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
var postResponse = mockMvc.perform(
post("/dcbService/transactions/{transactionId}?apiKey={apiKey}", TRANSACTION_ID, apiKey)
.content(asJsonString(createDcbTransaction()))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andReturn()
.getResponse();

Expand All @@ -123,6 +132,34 @@ void shouldConvertApiKeyToHeadersForPost() throws Exception {
assertThat(postResponse.getContentAsString()).isEqualTo(responseBody);
}

@Test
@SneakyThrows
void shouldConvertApiKeyToHeadersForRenewItemLoanByTransactionId() {
// Given
var apiKey = ApiKeyUtils.generateApiKey(10, TENANT, USERNAME);
setUpMockAuthnClient(TOKEN);
// When we make a valid request to mod-dcb with the API key set
mockDcbServer.enqueue(new MockResponse()
.setResponseCode(204)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON));

var putResponse = mockMvc.perform(
put("/dcbService/transactions/{transactionId}/renew?apiKey={apiKey}",
TRANSACTION_ID, apiKey)
.content(asJsonString(createDcbUpdateTransaction()))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andReturn()
.getResponse();

var headers = mockDcbServer.takeRequest().getHeaders();
assertThat(headers.get(XOkapiHeaders.TENANT)).isEqualTo(TENANT);
assertThat(headers.get(XOkapiHeaders.TOKEN)).isEqualTo(TOKEN);
assertThat(headers.get(XOkapiHeaders.USER_ID)).isNull();
assertThat(putResponse.getContentAsString()).isEmpty();
assertThat(putResponse.getStatus()).isEqualTo(204);
}

@Test
void shouldConvertApiKeyToHeadersForPutTransactionDetails() throws Exception {
// Given
Expand All @@ -132,10 +169,11 @@ void shouldConvertApiKeyToHeadersForPutTransactionDetails() throws Exception {
mockDcbServer.enqueue(new MockResponse()
.setResponseCode(204)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON));
var putResponse = mockMvc.perform(put("/dcbService/transactions/{transactionId}?apiKey={apiKey}", TRANSACTION_ID, apiKey)
.content(asJsonString(createDcbUpdateTransaction()))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
var putResponse = mockMvc.perform(
put("/dcbService/transactions/{transactionId}?apiKey={apiKey}", TRANSACTION_ID, apiKey)
.content(asJsonString(createDcbUpdateTransaction()))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andReturn()
.getResponse();

Expand All @@ -162,10 +200,11 @@ void shouldThrowErrorForInvalidRole() throws Exception {
.setResponseCode(201)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.setBody(responseBody));
var postResponse = mockMvc.perform(post("/dcbService/transactions/{transactionId}?apiKey={apiKey}", TRANSACTION_ID, apiKey)
.content(asJsonString(dcbTransaction + "\"role\" : \"\test\""))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
var postResponse = mockMvc.perform(
post("/dcbService/transactions/{transactionId}?apiKey={apiKey}", TRANSACTION_ID, apiKey)
.content(asJsonString(dcbTransaction + "\"role\" : \"\"test\""))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andReturn()
.getResponse();

Expand All @@ -185,10 +224,11 @@ void shouldThrowErrorForInvalidUUID() throws Exception {
.setResponseCode(201)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.setBody(responseBody));
var postResponse = mockMvc.perform(post("/dcbService/transactions/{transactionId}?apiKey={apiKey}", TRANSACTION_ID, apiKey)
.content(asJsonString(dcbTransaction ))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
var postResponse = mockMvc.perform(
post("/dcbService/transactions/{transactionId}?apiKey={apiKey}", TRANSACTION_ID, apiKey)
.content(asJsonString(dcbTransaction))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andReturn()
.getResponse();

Expand All @@ -206,21 +246,24 @@ void shouldThrowErrorForFeignException() throws Exception {
.errors(List.of(org.folio.ed.domain.dto.Error.builder()
.message("Unable to find Dcb transaction")
.build()))
.build();
.build();
mockDcbServer.enqueue(new MockResponse()
.setResponseCode(404)
.setBody(asJsonString(errors)));
var postResponse = mockMvc.perform(post("/dcbService/transactions/{transactionId}?apiKey={apiKey}", TRANSACTION_ID, apiKey)
.content(asJsonString(dcbTransaction))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
var postResponse = mockMvc.perform(
post("/dcbService/transactions/{transactionId}?apiKey={apiKey}", TRANSACTION_ID, apiKey)
.content(asJsonString(dcbTransaction))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andReturn()
.getResponse();
// As dcb will return Error message of type Errors, trying to assert the same here
var errorMessage = new ObjectMapper().readValue(postResponse.getContentAsString(), org.folio.ed.domain.dto.Errors.class);
var errorMessage = new ObjectMapper().readValue(postResponse.getContentAsString(),
org.folio.ed.domain.dto.Errors.class);

assertThat(postResponse.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
assertThat(errorMessage.getErrors().get(0).getMessage()).contains("Unable to find Dcb transaction");
assertThat(errorMessage.getErrors().get(0).getMessage()).contains(
"Unable to find Dcb transaction");
}

@Test
Expand All @@ -239,10 +282,12 @@ void shouldConvertApiKeyToHeadersForPut() throws Exception {
.setResponseCode(204)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.setBody(responseBody));
var putResponse = mockMvc.perform(put("/dcbService/transactions/{transactionId}/status?apiKey={apiKey}", transactionId, apiKey)
.content(asJsonString(createTransactionStatus(TransactionStatus.StatusEnum.AWAITING_PICKUP)))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
var putResponse = mockMvc.perform(
put("/dcbService/transactions/{transactionId}/status?apiKey={apiKey}", transactionId, apiKey)
.content(
asJsonString(createTransactionStatus(TransactionStatus.StatusEnum.AWAITING_PICKUP)))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andReturn()
.getResponse();

Expand Down Expand Up @@ -344,4 +389,30 @@ private void setUpMockAuthnClient(String token) {
public static String asJsonString(Object value) {
return OBJECT_MAPPER.writeValueAsString(value);
}

@Test
void shouldReturnClientErrorsWhenRenewLoanByTransactionId() throws Exception {
// Given
var apiKey = ApiKeyUtils.generateApiKey(10, TENANT, USERNAME);
var dcbResponseCode = HttpStatus.I_AM_A_TEAPOT.value(); // Arbitrary HTTP error status code
var dcbResponseBody = "I'm a teapot!";
setUpMockAuthnClient(TOKEN);

// When mod-dcb responds with an error
mockDcbServer.enqueue(new MockResponse()
.setResponseCode(dcbResponseCode)
.setBody(dcbResponseBody));
var putResponse = mockMvc.perform(put(
"/dcbService/transactions/{transactionId}/renew?apiKey={apiKey}",
TRANSACTION_ID, apiKey)
.content(asJsonString(createDcbUpdateTransaction()))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andReturn()
.getResponse();

// If mod-dcb doesn't return the error of type errors, then we edge-dcb will throw INTERNAL_SERVER_ERROR
assertThat(putResponse.getStatus()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value());
assertThat(putResponse.getContentAsString()).contains(dcbResponseBody);
}
}
17 changes: 17 additions & 0 deletions src/test/java/org/folio/ed/service/DcbTransactionServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.OffsetDateTime;

import static org.folio.ed.domain.dto.TransactionStatusResponse.RoleEnum.LENDER;
import static org.folio.ed.domain.dto.TransactionStatusResponse.StatusEnum.ITEM_CHECKED_OUT;
import static org.folio.ed.utils.EntityUtils.createDcbTransaction;
import static org.folio.ed.utils.EntityUtils.createDcbUpdateTransaction;
import static org.folio.ed.utils.EntityUtils.createTransactionStatus;
Expand Down Expand Up @@ -61,6 +63,21 @@ void updateDcbTransactionDetailsShouldThrowAnErrorIfClientReturnsError() {
() -> dcbTransactionService.updateTransactionDetails("123", dcbUpdateTransaction));
}

@Test
void renewLoanByTransactionIdTest() {
Mockito.when(dcbClient.renewLoanByTransactionId(anyString())).thenReturn(
createTransactionStatusResponse(ITEM_CHECKED_OUT, LENDER));
dcbTransactionService.renewLoanByTransactionId("123");
verify(dcbClient).renewLoanByTransactionId(anyString());
}

@Test
void renewLoanByTransactionIdShouldThrowAnErrorIfClientReturnsError() {
doThrow(IllegalArgumentException.class).when(dcbClient).renewLoanByTransactionId(anyString());
assertThrows(IllegalArgumentException.class,
() -> dcbTransactionService.renewLoanByTransactionId("123"));
}

@Test
void updateDcbTransactionStatusTest() {
Mockito.when(dcbClient.updateTransactionStatus(anyString(), any(TransactionStatus.class))).thenReturn(createTransactionStatusResponse(TransactionStatusResponse.StatusEnum.CREATED));
Expand Down
14 changes: 12 additions & 2 deletions src/test/java/org/folio/ed/utils/EntityUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import org.folio.ed.domain.dto.DcbItem;
import org.folio.ed.domain.dto.DcbPatron;
import org.folio.ed.domain.dto.DcbTransaction;
import org.folio.ed.domain.dto.DcbTransaction.RoleEnum;
import org.folio.ed.domain.dto.DcbUpdateTransaction;
import org.folio.ed.domain.dto.DcbUpdateItem;
import org.folio.ed.domain.dto.TransactionStatus;
import org.folio.ed.domain.dto.TransactionStatusResponse;
import org.folio.ed.domain.dto.TransactionStatusResponse.StatusEnum;

public class EntityUtils {

Expand All @@ -17,7 +19,7 @@ public static DcbTransaction createDcbTransaction() {
return DcbTransaction.builder()
.item(createDcbItem())
.patron(createDcbPatron())
.role(DcbTransaction.RoleEnum.LENDER)
.role(RoleEnum.LENDER)
.build();
}

Expand Down Expand Up @@ -54,7 +56,15 @@ public static DcbPatron createDcbPatron() {
.build();
}

public static TransactionStatusResponse createTransactionStatusResponse(TransactionStatusResponse.StatusEnum statusEnum){
public static TransactionStatusResponse createTransactionStatusResponse(StatusEnum statusEnum){
return TransactionStatusResponse.builder().status(statusEnum).build();
}

public static TransactionStatusResponse createTransactionStatusResponse(StatusEnum status,
TransactionStatusResponse.RoleEnum role) {
return TransactionStatusResponse.builder()
.status(status)
.role(role)
.build();
}
}

0 comments on commit 108fd8e

Please sign in to comment.