diff --git a/pom.xml b/pom.xml
index 3c61052..6675f69 100644
--- a/pom.xml
+++ b/pom.xml
@@ -70,6 +70,18 @@
2.6.3
test
+
+ com.squareup.okhttp3
+ okhttp
+ 4.0.1
+ test
+
+
+ com.squareup.okhttp3
+ mockwebserver
+ 4.0.1
+ test
+
org.projectlombok
lombok
diff --git a/src/main/java/com/productdock/adapter/in/web/DeleteBookApi.java b/src/main/java/com/productdock/adapter/in/web/DeleteBookApi.java
new file mode 100644
index 0000000..40d7e55
--- /dev/null
+++ b/src/main/java/com/productdock/adapter/in/web/DeleteBookApi.java
@@ -0,0 +1,20 @@
+package com.productdock.adapter.in.web;
+
+import com.productdock.application.port.in.DeleteBookUseCase;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/catalog/books")
+record DeleteBookApi(DeleteBookUseCase deleteBookUseCase) {
+
+ @DeleteMapping("/{bookId}")
+ public void deleteBook(@PathVariable("bookId") Long bookId) {
+ log.debug("DELETE BOOK request received with book id: {}", bookId);
+ deleteBookUseCase.deleteBook(bookId);
+ }
+}
diff --git a/src/main/java/com/productdock/adapter/out/kafka/DeletedBookMessagePublisher.java b/src/main/java/com/productdock/adapter/out/kafka/DeletedBookMessagePublisher.java
new file mode 100644
index 0000000..341b615
--- /dev/null
+++ b/src/main/java/com/productdock/adapter/out/kafka/DeletedBookMessagePublisher.java
@@ -0,0 +1,25 @@
+package com.productdock.adapter.out.kafka;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.productdock.adapter.out.kafka.publisher.KafkaPublisher;
+import com.productdock.application.port.out.messaging.DeleteBookMessagingOutPort;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.ExecutionException;
+
+@Component
+@RequiredArgsConstructor
+class DeletedBookMessagePublisher implements DeleteBookMessagingOutPort {
+
+ @Value("${spring.kafka.topic.delete-book}")
+ private String kafkaTopic;
+ private final KafkaPublisher publisher;
+
+ @Override
+ public void sendMessage(Long bookId) throws ExecutionException, InterruptedException, JsonProcessingException {
+ publisher.sendMessage(bookId, kafkaTopic);
+ }
+
+}
diff --git a/src/main/java/com/productdock/adapter/out/sql/BookPersistenceAdapter.java b/src/main/java/com/productdock/adapter/out/sql/BookPersistenceAdapter.java
index a2fc99a..777f19b 100644
--- a/src/main/java/com/productdock/adapter/out/sql/BookPersistenceAdapter.java
+++ b/src/main/java/com/productdock/adapter/out/sql/BookPersistenceAdapter.java
@@ -45,6 +45,11 @@ public Book save(Book book) {
return bookMapper.toDomain(bookRepository.save(bookJpaEntity));
}
+ @Override
+ public void deleteById(Long bookId) {
+ bookRepository.deleteById(bookId);
+ }
+
private Set populateBookTopics(List topics) {
var topicEntities = topicRepository.findByIds(topics.stream().map(Book.Topic::getId).toList());
if (topics.size() != topicEntities.size())
diff --git a/src/main/java/com/productdock/adapter/out/web/RentalsApiClient.java b/src/main/java/com/productdock/adapter/out/web/RentalsApiClient.java
new file mode 100644
index 0000000..7cf12db
--- /dev/null
+++ b/src/main/java/com/productdock/adapter/out/web/RentalsApiClient.java
@@ -0,0 +1,51 @@
+package com.productdock.adapter.out.web;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.productdock.application.port.out.web.RentalsClient;
+import com.productdock.domain.BookRentalState;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.stereotype.Component;
+import org.springframework.web.util.DefaultUriBuilderFactory;
+
+import java.io.IOException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Collection;
+
+@Slf4j
+@Component
+public class RentalsApiClient implements RentalsClient {
+
+ private String rentalsServiceUrl;
+ private HttpClient client = HttpClient.newHttpClient();
+
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ public RentalsApiClient(@Value("${rental.service.url}/api/rental/book/") String rentalsServiceUrl) {
+ this.rentalsServiceUrl = rentalsServiceUrl;
+ }
+
+ @Override
+ public Collection getRentals(Long bookId) throws IOException, InterruptedException {
+ var jwt = ((Jwt) SecurityContextHolder.getContext().getAuthentication().getCredentials()).getTokenValue();
+ var uri = new DefaultUriBuilderFactory(rentalsServiceUrl)
+ .builder()
+ .path(bookId.toString())
+ .path("/rentals")
+ .build();
+ var request = HttpRequest.newBuilder()
+ .uri(uri)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt)
+ .GET()
+ .build();
+ var response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ return objectMapper.readValue(response.body(), new TypeReference<>() {
+ });
+ }
+}
diff --git a/src/main/java/com/productdock/application/port/in/DeleteBookUseCase.java b/src/main/java/com/productdock/application/port/in/DeleteBookUseCase.java
new file mode 100644
index 0000000..f937203
--- /dev/null
+++ b/src/main/java/com/productdock/application/port/in/DeleteBookUseCase.java
@@ -0,0 +1,6 @@
+package com.productdock.application.port.in;
+
+public interface DeleteBookUseCase {
+
+ void deleteBook(Long bookId);
+}
diff --git a/src/main/java/com/productdock/application/port/out/messaging/DeleteBookMessagingOutPort.java b/src/main/java/com/productdock/application/port/out/messaging/DeleteBookMessagingOutPort.java
new file mode 100644
index 0000000..42862fa
--- /dev/null
+++ b/src/main/java/com/productdock/application/port/out/messaging/DeleteBookMessagingOutPort.java
@@ -0,0 +1,10 @@
+package com.productdock.application.port.out.messaging;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+import java.util.concurrent.ExecutionException;
+
+public interface DeleteBookMessagingOutPort {
+ void sendMessage(Long bookId) throws ExecutionException, InterruptedException, JsonProcessingException;
+
+}
diff --git a/src/main/java/com/productdock/application/port/out/persistence/BookPersistenceOutPort.java b/src/main/java/com/productdock/application/port/out/persistence/BookPersistenceOutPort.java
index c4d957c..8e2dd51 100644
--- a/src/main/java/com/productdock/application/port/out/persistence/BookPersistenceOutPort.java
+++ b/src/main/java/com/productdock/application/port/out/persistence/BookPersistenceOutPort.java
@@ -11,4 +11,6 @@ public interface BookPersistenceOutPort {
Optional findByTitleAndAuthor(String title, String author);
Book save(Book book);
+
+ void deleteById(Long bookId);
}
diff --git a/src/main/java/com/productdock/application/port/out/web/RentalsClient.java b/src/main/java/com/productdock/application/port/out/web/RentalsClient.java
new file mode 100644
index 0000000..5b7e61a
--- /dev/null
+++ b/src/main/java/com/productdock/application/port/out/web/RentalsClient.java
@@ -0,0 +1,11 @@
+package com.productdock.application.port.out.web;
+
+import com.productdock.domain.BookRentalState;
+
+import java.io.IOException;
+import java.util.Collection;
+
+public interface RentalsClient {
+
+ Collection getRentals(Long bookId) throws IOException, InterruptedException;
+}
diff --git a/src/main/java/com/productdock/application/service/DeleteBookService.java b/src/main/java/com/productdock/application/service/DeleteBookService.java
new file mode 100644
index 0000000..76c952b
--- /dev/null
+++ b/src/main/java/com/productdock/application/service/DeleteBookService.java
@@ -0,0 +1,63 @@
+package com.productdock.application.service;
+
+import com.productdock.application.port.in.DeleteBookUseCase;
+import com.productdock.application.port.out.messaging.DeleteBookMessagingOutPort;
+import com.productdock.application.port.out.persistence.BookPersistenceOutPort;
+import com.productdock.application.port.out.web.RentalsClient;
+import com.productdock.domain.BookRentalState;
+import com.productdock.domain.exception.BookNotFoundException;
+import com.productdock.domain.exception.DeleteBookException;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collection;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+class DeleteBookService implements DeleteBookUseCase {
+
+ private final BookPersistenceOutPort bookRepository;
+
+ private final DeleteBookMessagingOutPort deleteBookMessagingOutPort;
+
+ private final RentalsClient rentalsClient;
+
+ @Override
+ @SneakyThrows
+ @Transactional
+ public void deleteBook(Long bookId) {
+ validateBookAvailability(bookId);
+ bookRepository.deleteById(bookId);
+ deleteBookMessagingOutPort.sendMessage(bookId);
+ log.debug("deleted book with id: {}", bookId);
+ }
+
+ @SneakyThrows
+ private void validateBookAvailability(Long bookId) {
+ if (bookRepository.findById(bookId).isEmpty()) {
+ throw new BookNotFoundException("Book not found.");
+ }
+ var bookRentals = rentalsClient.getRentals(bookId);
+ if (!bookRentals.isEmpty()) {
+ throw new DeleteBookException(createRentalMessage(bookRentals));
+ }
+ }
+
+ private String createRentalMessage(Collection bookRentals) {
+ var message = "Book in use by: ";
+ var punctuation = "";
+ for (var rental : bookRentals) {
+ var status = rental.status().toString().toLowerCase();
+ var userName = rental.user().fullName();
+
+ message = message.concat(punctuation).concat(userName).concat(" (").concat(status).concat(")");
+ punctuation = ", ";
+ }
+
+ return message;
+ }
+}
diff --git a/src/main/java/com/productdock/config/SecurityConfig.java b/src/main/java/com/productdock/config/SecurityConfig.java
index 0dc46a4..4718de3 100644
--- a/src/main/java/com/productdock/config/SecurityConfig.java
+++ b/src/main/java/com/productdock/config/SecurityConfig.java
@@ -16,6 +16,8 @@
@RequiredArgsConstructor
@Configuration
public class SecurityConfig {
+
+ private static final String ROLE_ADMIN = "SCOPE_ROLE_ADMIN";
@Value("${jwt.public.key}")
RSAPublicKey key;
@@ -23,7 +25,8 @@ public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(authorize -> authorize.antMatchers("/actuator/**").permitAll()
- .antMatchers(HttpMethod.POST, "/api/catalog/books").hasAuthority("SCOPE_ROLE_ADMIN")
+ .antMatchers(HttpMethod.POST, "/api/catalog/books").hasAuthority(ROLE_ADMIN)
+ .antMatchers(HttpMethod.DELETE, "/api/catalog/books/{bookId}").hasAuthority(ROLE_ADMIN)
.anyRequest().authenticated())
.cors().and()
.oauth2ResourceServer().jwt();
diff --git a/src/main/java/com/productdock/domain/BookRentalState.java b/src/main/java/com/productdock/domain/BookRentalState.java
new file mode 100644
index 0000000..df0b6ab
--- /dev/null
+++ b/src/main/java/com/productdock/domain/BookRentalState.java
@@ -0,0 +1,7 @@
+package com.productdock.domain;
+
+import java.util.Date;
+
+public record BookRentalState(UserProfile user, RentalStatus status, Date date) {
+}
+
diff --git a/src/main/java/com/productdock/domain/RentalStatus.java b/src/main/java/com/productdock/domain/RentalStatus.java
new file mode 100644
index 0000000..56943d6
--- /dev/null
+++ b/src/main/java/com/productdock/domain/RentalStatus.java
@@ -0,0 +1,6 @@
+package com.productdock.domain;
+
+public enum RentalStatus {
+ RENTED,
+ RESERVED
+}
diff --git a/src/main/java/com/productdock/domain/UserProfile.java b/src/main/java/com/productdock/domain/UserProfile.java
new file mode 100644
index 0000000..d48d347
--- /dev/null
+++ b/src/main/java/com/productdock/domain/UserProfile.java
@@ -0,0 +1,5 @@
+package com.productdock.domain;
+
+
+public record UserProfile(String fullName, String image, String email) {
+}
diff --git a/src/main/java/com/productdock/domain/exception/BookNotFoundException.java b/src/main/java/com/productdock/domain/exception/BookNotFoundException.java
new file mode 100644
index 0000000..797d7d2
--- /dev/null
+++ b/src/main/java/com/productdock/domain/exception/BookNotFoundException.java
@@ -0,0 +1,10 @@
+package com.productdock.domain.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.NOT_FOUND)
+public class BookNotFoundException extends RuntimeException{
+
+ public BookNotFoundException(String message){ super(message);}
+}
diff --git a/src/main/java/com/productdock/domain/exception/DeleteBookException.java b/src/main/java/com/productdock/domain/exception/DeleteBookException.java
new file mode 100644
index 0000000..9e4f49d
--- /dev/null
+++ b/src/main/java/com/productdock/domain/exception/DeleteBookException.java
@@ -0,0 +1,10 @@
+package com.productdock.domain.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.BAD_REQUEST)
+public class DeleteBookException extends RuntimeException {
+
+ public DeleteBookException(String message){ super(message);}
+}
diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml
index 28264dc..198eb66 100644
--- a/src/main/resources/application-local.yaml
+++ b/src/main/resources/application-local.yaml
@@ -1,4 +1,5 @@
USER_PROFILES_JWT_PUBLIC_KEY: src/main/resources/app-local.pub
+RENTAL_SERVICE_URL: http://localhost:8083
KAFKA_SERVER_URL: localhost:9093
MYSQL_SERVER_URL: localhost:3308
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 4a83e3b..9deca14 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -22,6 +22,7 @@ spring:
topic:
book-rating: book-rating
insert-book: insert-book
+ delete-book: delete-book
jpa:
hibernate:
@@ -40,6 +41,10 @@ file-logging-enabled: ${LOG_TO_FILE}
jwt:
public.key: file:${USER_PROFILES_JWT_PUBLIC_KEY}
+rental:
+ service:
+ url: ${RENTAL_SERVICE_URL}
+
management:
server:
port: 8087
diff --git a/src/test/java/com/productdock/adapter/in/web/DeleteBookApiShould.java b/src/test/java/com/productdock/adapter/in/web/DeleteBookApiShould.java
new file mode 100644
index 0000000..9836df1
--- /dev/null
+++ b/src/test/java/com/productdock/adapter/in/web/DeleteBookApiShould.java
@@ -0,0 +1,28 @@
+package com.productdock.adapter.in.web;
+
+import com.productdock.application.port.in.DeleteBookUseCase;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+public class DeleteBookApiShould {
+
+ public static final long BOOK_ID = 1;
+ @InjectMocks
+ private DeleteBookApi deleteBookApi;
+ @Mock
+ private DeleteBookUseCase deleteBookUseCase;
+
+ @Test
+ void deleteBook() {
+
+ deleteBookApi.deleteBook(BOOK_ID);
+
+ verify(deleteBookUseCase).deleteBook(BOOK_ID);
+ }
+}
diff --git a/src/test/java/com/productdock/adapter/out/kafka/DeleteBookMessagePublisherShould.java b/src/test/java/com/productdock/adapter/out/kafka/DeleteBookMessagePublisherShould.java
new file mode 100644
index 0000000..dc66da5
--- /dev/null
+++ b/src/test/java/com/productdock/adapter/out/kafka/DeleteBookMessagePublisherShould.java
@@ -0,0 +1,31 @@
+package com.productdock.adapter.out.kafka;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.productdock.adapter.out.kafka.publisher.KafkaPublisher;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+public class DeleteBookMessagePublisherShould {
+ public static final long BOOK_ID = 1;
+
+ @InjectMocks
+ private DeletedBookMessagePublisher deletedBookMessagePublisher;
+ @Mock
+ private KafkaPublisher publisher;
+
+ @Test
+ void sendMessage() throws ExecutionException, InterruptedException, JsonProcessingException {
+
+ deletedBookMessagePublisher.sendMessage(BOOK_ID);
+
+ verify(publisher).sendMessage(BOOK_ID, null);
+ }
+}
diff --git a/src/test/java/com/productdock/application/service/DeleteBookServiceShould.java b/src/test/java/com/productdock/application/service/DeleteBookServiceShould.java
new file mode 100644
index 0000000..3469f50
--- /dev/null
+++ b/src/test/java/com/productdock/application/service/DeleteBookServiceShould.java
@@ -0,0 +1,78 @@
+package com.productdock.application.service;
+
+import com.productdock.application.port.out.messaging.DeleteBookMessagingOutPort;
+import com.productdock.application.port.out.persistence.BookPersistenceOutPort;
+import com.productdock.application.port.out.web.RentalsClient;
+import com.productdock.domain.Book;
+import com.productdock.domain.BookRentalState;
+import com.productdock.domain.RentalStatus;
+import com.productdock.domain.UserProfile;
+import com.productdock.domain.exception.BookNotFoundException;
+import com.productdock.domain.exception.DeleteBookException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+class DeleteBookServiceShould {
+
+ private static final UserProfile USER_PROFILE = new UserProfile("Mocked name", null, null);
+ private static final BookRentalState RENTAL_STATE = new BookRentalState(USER_PROFILE, RentalStatus.RENTED, null);
+
+ private static final String EXCEPTION_MESSAGE = "Book in use by: Mocked name (rented)";
+ private static Collection RENTALS = new ArrayList<>();
+ private static final Optional BOOK = Optional.of(mock(Book.class));
+ private static final Long BOOK_ID = 1L;
+
+ @InjectMocks
+ private DeleteBookService deleteBookService;
+ @Mock
+ private BookPersistenceOutPort bookRepository;
+ @Mock
+ private RentalsClient rentalsClient;
+ @Mock
+ private DeleteBookMessagingOutPort deleteBookMessagingOutPort;
+
+ @Test
+ void deleteBookWhenIdExist() {
+
+ given(bookRepository.findById(BOOK_ID)).willReturn(BOOK);
+
+ deleteBookService.deleteBook(BOOK_ID);
+
+ verify(bookRepository).deleteById(BOOK_ID);
+ }
+
+ @Test
+ void throwExceptionWhenBookDoesntExist() {
+ given(bookRepository.findById(BOOK_ID)).willReturn(Optional.empty());
+
+ assertThatThrownBy(() -> deleteBookService.deleteBook(BOOK_ID))
+ .isInstanceOf(BookNotFoundException.class);
+ }
+
+ @Test
+ void throwExceptionWhenBookIsTaken() throws IOException, InterruptedException {
+ RENTALS.add(RENTAL_STATE);
+
+ given(bookRepository.findById(BOOK_ID)).willReturn(BOOK);
+
+ given(rentalsClient.getRentals(BOOK_ID)).willReturn(RENTALS);
+
+ assertThatThrownBy(() -> deleteBookService.deleteBook(BOOK_ID))
+ .isInstanceOf(DeleteBookException.class)
+ .hasMessage(EXCEPTION_MESSAGE);
+ }
+}
diff --git a/src/test/java/com/productdock/data/provider/out/kafka/KafkaTestConsumer.java b/src/test/java/com/productdock/data/provider/out/kafka/KafkaTestConsumer.java
index 654a118..bef2408 100644
--- a/src/test/java/com/productdock/data/provider/out/kafka/KafkaTestConsumer.java
+++ b/src/test/java/com/productdock/data/provider/out/kafka/KafkaTestConsumer.java
@@ -35,6 +35,12 @@ public void receiveInsertBook(ConsumerRecord consumerInsertBook)
writeRecordToFile(insertBookMessage, "testAddBook.txt");
}
+ @KafkaListener(topics = "${spring.kafka.topic.delete-book}")
+ public void recieveDeleteBook(ConsumerRecord consumerRecord) throws JsonProcessingException{
+ var deleteBookMessage = kafkaMessageDeserializer.deserializeDeleteBookMessage(consumerRecord);
+ writeRecordToFile(deleteBookMessage, "testDeleteBook.txt");
+ }
+
private void writeRecordToFile(Object message, String fileName) {
try {
FileOutputStream fileOutputStream = new FileOutputStream(fileName);
diff --git a/src/test/java/com/productdock/integration/DeleteBookApiTest.java b/src/test/java/com/productdock/integration/DeleteBookApiTest.java
new file mode 100644
index 0000000..5761786
--- /dev/null
+++ b/src/test/java/com/productdock/integration/DeleteBookApiTest.java
@@ -0,0 +1,120 @@
+package com.productdock.integration;
+
+import com.productdock.adapter.out.sql.BookRepository;
+import com.productdock.adapter.out.sql.TopicRepository;
+import com.productdock.adapter.out.sql.entity.TopicJpaEntity;
+import com.productdock.data.provider.out.kafka.KafkaTestBase;
+import lombok.SneakyThrows;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+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.jdbc.core.JdbcTemplate;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.jdbc.JdbcTestUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.time.Duration;
+
+import static com.productdock.data.provider.out.sql.BookEntityMother.defaultBookEntityBuilder;
+import static com.productdock.kafka.KafkaFileUtil.getMessageFrom;
+import static com.productdock.kafka.KafkaFileUtil.ifFileExists;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest
+@ActiveProfiles({"in-memory-db"})
+public class DeleteBookApiTest extends KafkaTestBase {
+
+ public static final String TEST_FILE = "testDeleteBook.txt";
+ private static final String ROLE_ADMIN = "SCOPE_ROLE_ADMIN";
+ private static final String ROLE_USER = "SCOPE_ROLE_USER";
+
+ public static MockWebServer mockRentalBackEnd;
+ @Autowired
+ private BookRepository bookRepository;
+ @Autowired
+ private TopicRepository topicRepository;
+ @Autowired
+ private RestRequestProducer requestProducer;
+ @Autowired
+ private JdbcTemplate jdbcTemplate;
+
+ @BeforeAll
+ static void setUp() throws IOException {
+ mockRentalBackEnd = new MockWebServer();
+ mockRentalBackEnd.start(8083);
+ }
+
+ @AfterAll
+ static void tearDown() throws IOException {
+ mockRentalBackEnd.shutdown();
+ }
+
+ @AfterEach
+ final void before() {
+ JdbcTestUtils.deleteFromTables(jdbcTemplate, "book_topic", "book", "topic");
+ }
+
+ @AfterAll
+ static void after() {
+ new File(TEST_FILE).delete();
+ }
+
+ @Test
+ @WithMockUser
+ @SneakyThrows
+ void deleteBook_whenIdDoesntExist() {
+ requestProducer.makeDeleteBookRequest(1L, ROLE_ADMIN).andExpect(status().isNotFound());
+ }
+
+ @Test
+ @WithMockUser
+ void returnForbidden_whenUserWithInsufficientRole() throws Exception {
+ requestProducer.makeDeleteBookRequest(1L, ROLE_USER)
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ @WithMockUser
+ void deleteBook_whenBookIsValid() throws Exception {
+ var bookId = givenAnyBook();
+ mockRentalBackEnd.enqueue(new MockResponse().setBody("[]").addHeader("Content-Type", "application/json"));
+ requestProducer.makeDeleteBookRequest(bookId, ROLE_ADMIN).andExpect(status().isOk());
+
+ await()
+ .atMost(Duration.ofSeconds(4))
+ .until(ifFileExists(TEST_FILE));
+
+ var deleteBookMessage = (Long) getMessageFrom(TEST_FILE);
+ assertThat(deleteBookMessage).isEqualTo(bookId);
+ }
+
+ @Test
+ @WithMockUser
+ void deleteBook_whenBookIsTaken() throws Exception {
+ var bookId = givenAnyBook();
+ mockRentalBackEnd.enqueue(new MockResponse().setBody("[{\"user\":{\"fullName\":\"Test Test\"},\"status\":\"RENTED\"}]").addHeader("Content-Type", "application/json"));
+ requestProducer.makeDeleteBookRequest(bookId, ROLE_ADMIN).andExpect(status().isBadRequest());
+ }
+
+ private Long givenAnyBook() {
+ var marketingTopic = givenTopicWithName("MARKETING");
+ var designTopic = givenTopicWithName("DESIGN");
+ var book = defaultBookEntityBuilder().topic(marketingTopic).topic(designTopic).build();
+ return bookRepository.save(book).getId();
+ }
+
+ private TopicJpaEntity givenTopicWithName(String name) {
+ var topic = TopicJpaEntity.builder().name(name).build();
+ return topicRepository.save(topic);
+ }
+
+}
diff --git a/src/test/java/com/productdock/integration/RestRequestProducer.java b/src/test/java/com/productdock/integration/RestRequestProducer.java
index c6b9e3d..093ca14 100644
--- a/src/test/java/com/productdock/integration/RestRequestProducer.java
+++ b/src/test/java/com/productdock/integration/RestRequestProducer.java
@@ -86,4 +86,12 @@ public ResultActions makeGetTopicsRequest() throws Exception {
})))
.andExpect(status().isOk());
}
+
+ public ResultActions makeDeleteBookRequest(Long bookId, String role) throws Exception {
+ return mockMvc.perform(delete("/api/catalog/books/" + bookId)
+ .with(jwt().jwt(jwt -> {
+ jwt.claim("email", DEFAULT_USER_ID);
+ jwt.claim("fullName", "::userFullName");
+ }).authorities(new SimpleGrantedAuthority(role))));
+ }
}
diff --git a/src/test/java/com/productdock/kafka/KafkaMessageDeserializer.java b/src/test/java/com/productdock/kafka/KafkaMessageDeserializer.java
index 82dd906..5377e0d 100644
--- a/src/test/java/com/productdock/kafka/KafkaMessageDeserializer.java
+++ b/src/test/java/com/productdock/kafka/KafkaMessageDeserializer.java
@@ -17,4 +17,8 @@ public BookRatingMessage deserializeBookRatingMessage(ConsumerRecord consumerInsertBook) throws JsonProcessingException {
return objectMapper.readValue(consumerInsertBook.value(), AddedBookMessage.class);
}
+
+ public Long deserializeDeleteBookMessage(ConsumerRecord consumerRecord) throws JsonProcessingException {
+ return objectMapper.readValue(consumerRecord.value(),Long.class);
+ }
}
diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml
index e9b8f13..efaf862 100644
--- a/src/test/resources/application.yaml
+++ b/src/test/resources/application.yaml
@@ -26,10 +26,15 @@ spring:
topic:
book-rating: embedded-book-rating
insert-book: embedded-insert-book
+ delete-book: embedded-delete-book
cors:
allowed:
origins: localhost:3000
jwt:
- public.key: classpath:test.pub
\ No newline at end of file
+ public.key: classpath:test.pub
+
+rental:
+ service:
+ url: http://localhost:8083
\ No newline at end of file