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