From 55bb53d47c4561917539377d1d7eb343112a4223 Mon Sep 17 00:00:00 2001 From: asteriskzie Date: Wed, 1 May 2024 20:27:50 +0700 Subject: [PATCH 01/17] [FIX] Fix cd workflow --- .github/workflows/cd.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 47d1fb0..1153188 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -45,6 +45,16 @@ jobs: name: Test runs-on: ubuntu-latest needs: build + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: snackscription_review + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout Repository uses: actions/checkout@v4 From 5518184933b3197eac709d7e073426b235ec7677 Mon Sep 17 00:00:00 2001 From: asteriskzie Date: Wed, 8 May 2024 20:13:44 +0700 Subject: [PATCH 02/17] Add async function --- .../review/ReviewApplication.java | 18 ++++++++++++++++-- .../review/service/ReviewService.java | 9 +++++++++ .../service/SentimentAnalysisService.java | 8 ++++++++ .../review/service/ReviewServiceTest.java | 5 +++++ 4 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/main/java/snackscription/review/service/SentimentAnalysisService.java diff --git a/src/main/java/snackscription/review/ReviewApplication.java b/src/main/java/snackscription/review/ReviewApplication.java index ec64e1c..4898e1d 100644 --- a/src/main/java/snackscription/review/ReviewApplication.java +++ b/src/main/java/snackscription/review/ReviewApplication.java @@ -2,14 +2,28 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import java.util.concurrent.Executor; + +@EnableAsync @SpringBootApplication public class ReviewApplication { public static void main(String[] args) { SpringApplication.run(ReviewApplication.class, args); - - } + @Bean + public Executor taskExecutor () { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(1); + executor.setMaxPoolSize(1); + executor.setQueueCapacity(500); + executor.setThreadNamePrefix("GithubLookup-"); + executor.initialize(); + return executor; + } } diff --git a/src/main/java/snackscription/review/service/ReviewService.java b/src/main/java/snackscription/review/service/ReviewService.java index bcd511a..266d3ba 100644 --- a/src/main/java/snackscription/review/service/ReviewService.java +++ b/src/main/java/snackscription/review/service/ReviewService.java @@ -2,7 +2,9 @@ import java.util.List; import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import snackscription.review.exception.InvalidStateException; @@ -14,6 +16,7 @@ @Service public class ReviewService { private ReviewRepository reviewRepository; + private SentimentAnalysisService sentimentAnalysisService; public ReviewService (ReviewRepository reviewRepository) { this.reviewRepository = reviewRepository; @@ -90,4 +93,10 @@ public Review rejectReview(String reviewId) throws Exception { review.reject(); return reviewRepository.save(review); } + + @Async + public CompletableFuture analyzeSentimentAsync(String reviewText) { + String sentiment = sentimentAnalysisService.analyze(reviewText); + return CompletableFuture.completedFuture(sentiment); + } } diff --git a/src/main/java/snackscription/review/service/SentimentAnalysisService.java b/src/main/java/snackscription/review/service/SentimentAnalysisService.java new file mode 100644 index 0000000..b9610a8 --- /dev/null +++ b/src/main/java/snackscription/review/service/SentimentAnalysisService.java @@ -0,0 +1,8 @@ +package snackscription.review.service; + +public class SentimentAnalysisService { + public String analyze(String reviewText) { + return "positive"; + } + +} diff --git a/src/test/java/snackscription/review/service/ReviewServiceTest.java b/src/test/java/snackscription/review/service/ReviewServiceTest.java index 264cb15..2187307 100644 --- a/src/test/java/snackscription/review/service/ReviewServiceTest.java +++ b/src/test/java/snackscription/review/service/ReviewServiceTest.java @@ -241,5 +241,10 @@ public void assertEqualReview(Review review1, Review review2) { assertEquals(review1.getUserId(), review2.getUserId()); assertEquals(review1.getSubscriptionBoxId(), review2.getSubscriptionBoxId()); } + + @Test + public void analyzeSentimentAsyncTest() { + + } } From 6c563828ded34d5fe552e4be1619d21fa024dec7 Mon Sep 17 00:00:00 2001 From: asteriskzie Date: Wed, 8 May 2024 20:50:02 +0700 Subject: [PATCH 03/17] [RED] Add test for sentiment analysis async --- .../service/SentimentAnalysisService.java | 1 + .../review/service/ReviewServiceTest.java | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/snackscription/review/service/SentimentAnalysisService.java b/src/main/java/snackscription/review/service/SentimentAnalysisService.java index b9610a8..1ee68cf 100644 --- a/src/main/java/snackscription/review/service/SentimentAnalysisService.java +++ b/src/main/java/snackscription/review/service/SentimentAnalysisService.java @@ -2,6 +2,7 @@ public class SentimentAnalysisService { public String analyze(String reviewText) { + // do analysis return "positive"; } diff --git a/src/test/java/snackscription/review/service/ReviewServiceTest.java b/src/test/java/snackscription/review/service/ReviewServiceTest.java index 2187307..e2f397d 100644 --- a/src/test/java/snackscription/review/service/ReviewServiceTest.java +++ b/src/test/java/snackscription/review/service/ReviewServiceTest.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import javax.naming.spi.DirStateFactory.Result; import javax.swing.text.html.Option; @@ -26,10 +27,13 @@ import snackscription.review.model.Review; import snackscription.review.model.ReviewState; import snackscription.review.repository.ReviewRepository; +import snackscription.review.service.SentimentAnalysisService; @ExtendWith(MockitoExtension.class) public class ReviewServiceTest { - + + @Mock + SentimentAnalysisService sentimentAnalysisService; @Mock ReviewRepository reviewRepo; @@ -58,6 +62,7 @@ public class ReviewServiceTest { @BeforeEach public void setUp() { reviewService = new ReviewService(reviewRepo); + sentimentAnalysisService = new SentimentAnalysisService(); Review review1 = new Review(5, "I love it", "user_123", "subsbox_123"); Review review2 = new Review(1, "I hate it", "user_124", "subsbox_123"); @@ -243,8 +248,18 @@ public void assertEqualReview(Review review1, Review review2) { } @Test - public void analyzeSentimentAsyncTest() { + public void testAnalyzeSentimentAsyncTest() { + String reviewText = "This is a great product!"; + String expectedSentiment = "positive"; + + when(sentimentAnalysisService.analyze(reviewText)).thenReturn(expectedSentiment); + + CompletableFuture sentimentFuture = reviewService.analyzeSentimentAsync(reviewText); + + String actualSentiment = sentimentFuture.join(); + verify(sentimentAnalysisService).analyze(reviewText); + assertEquals(expectedSentiment, actualSentiment); } } From 4bc3c8f42e51632739b897d4bed9b82b7ca6cd27 Mon Sep 17 00:00:00 2001 From: asteriskzie Date: Sun, 19 May 2024 19:34:28 +0700 Subject: [PATCH 04/17] Revert "Merge pull request #12 from ADPRO-C11/async" This reverts commit 697428ab5c0148081c460742bfc80746f7c5d590, reversing changes made to 130fa41cca4c54eaae4033be3ffbacf5cb2631b6. The Sonarcloud setting is not working yet, fixing it soon. --- .github/workflows/cd.yml | 10 ---------- .../review/ReviewApplication.java | 18 ++---------------- .../review/service/ReviewService.java | 10 ---------- .../service/SentimentAnalysisService.java | 8 -------- .../review/service/ReviewServiceTest.java | 5 ----- 5 files changed, 2 insertions(+), 49 deletions(-) delete mode 100644 src/main/java/snackscription/review/service/SentimentAnalysisService.java diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 40fcb7c..30e9471 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -45,16 +45,6 @@ jobs: name: Test runs-on: ubuntu-latest needs: build - services: - postgres: - image: postgres:latest - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: snackscription_review - ports: - - 5432:5432 - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout Repository uses: actions/checkout@v4 diff --git a/src/main/java/snackscription/review/ReviewApplication.java b/src/main/java/snackscription/review/ReviewApplication.java index 4898e1d..ec64e1c 100644 --- a/src/main/java/snackscription/review/ReviewApplication.java +++ b/src/main/java/snackscription/review/ReviewApplication.java @@ -2,28 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import java.util.concurrent.Executor; - -@EnableAsync @SpringBootApplication public class ReviewApplication { public static void main(String[] args) { SpringApplication.run(ReviewApplication.class, args); - } - @Bean - public Executor taskExecutor () { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(1); - executor.setMaxPoolSize(1); - executor.setQueueCapacity(500); - executor.setThreadNamePrefix("GithubLookup-"); - executor.initialize(); - return executor; + } + } diff --git a/src/main/java/snackscription/review/service/ReviewService.java b/src/main/java/snackscription/review/service/ReviewService.java index 44883b9..31a94c5 100644 --- a/src/main/java/snackscription/review/service/ReviewService.java +++ b/src/main/java/snackscription/review/service/ReviewService.java @@ -2,9 +2,6 @@ import java.util.List; import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -import org.springframework.scheduling.annotation.Async; import java.util.concurrent.atomic.AtomicInteger; import io.micrometer.core.instrument.composite.CompositeMeterRegistry; @@ -22,7 +19,6 @@ @Component public class ReviewService { private ReviewRepository reviewRepository; - private SentimentAnalysisService sentimentAnalysisService; public ReviewService (ReviewRepository reviewRepository) { this.reviewRepository = reviewRepository; @@ -80,12 +76,6 @@ public Review rejectReview(String subsbox, String user) throws Exception { return reviewRepository.save(review); } - @Async - public CompletableFuture analyzeSentimentAsync(String reviewText) { - String sentiment = sentimentAnalysisService.analyze(reviewText); - return CompletableFuture.completedFuture(sentiment); - } - public void deleteReview(String subsbox, String user) throws Exception { Review review = reviewRepository.findByIdSubsboxAndIdAuthor(subsbox, user); diff --git a/src/main/java/snackscription/review/service/SentimentAnalysisService.java b/src/main/java/snackscription/review/service/SentimentAnalysisService.java deleted file mode 100644 index b9610a8..0000000 --- a/src/main/java/snackscription/review/service/SentimentAnalysisService.java +++ /dev/null @@ -1,8 +0,0 @@ -package snackscription.review.service; - -public class SentimentAnalysisService { - public String analyze(String reviewText) { - return "positive"; - } - -} diff --git a/src/test/java/snackscription/review/service/ReviewServiceTest.java b/src/test/java/snackscription/review/service/ReviewServiceTest.java index 4fdf16d..6350528 100644 --- a/src/test/java/snackscription/review/service/ReviewServiceTest.java +++ b/src/test/java/snackscription/review/service/ReviewServiceTest.java @@ -179,10 +179,5 @@ public void assertEqualReview(Review review1, Review review2) { assertEquals(review1.getAuthor(), review2.getAuthor()); assertEquals(review1.getSubsbox(), review2.getSubsbox()); } - - @Test - public void analyzeSentimentAsyncTest() { - - } } From c4e50d3daee45f06c18df7c2cfdff95a18a9400c Mon Sep 17 00:00:00 2001 From: asteriskzie Date: Mon, 20 May 2024 10:35:15 +0700 Subject: [PATCH 05/17] Configure prometheus to monitor deployed app --- .monitoring/prometheus/prometheus.yml | 7 +++++-- build.gradle.kts | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.monitoring/prometheus/prometheus.yml b/.monitoring/prometheus/prometheus.yml index 94f2cfe..f454fff 100644 --- a/.monitoring/prometheus/prometheus.yml +++ b/.monitoring/prometheus/prometheus.yml @@ -1,8 +1,11 @@ scrape_configs: - - job_name: 'MyAppMetrics' + - job_name: 'Snackscription Metrics' metrics_path: '/actuator/prometheus' scrape_interval: 3s static_configs: - targets: ['host.docker.internal:8080'] labels: - application: 'Snackscription Review' \ No newline at end of file + application: 'Snackscription Review' + - targets: ['34.124.152.90'] + labels: + application: 'Snackscription Review (deployed)' \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 01cc3e8..0acc097 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,10 +27,11 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("io.micrometer:micrometer-registry-prometheus") + implementation("io.micrometer:micrometer-core") compileOnly("org.projectlombok:lombok") developmentOnly("org.springframework.boot:spring-boot-devtools") runtimeOnly("org.postgresql:postgresql") - runtimeOnly("io.micrometer:micrometer-registry-prometheus") annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") annotationProcessor("org.projectlombok:lombok") testImplementation("org.springframework.boot:spring-boot-starter-test") From fdd575dc9e12abc91ea4a260ebc5f4e7d7f9425a Mon Sep 17 00:00:00 2001 From: asteriskzie Date: Mon, 20 May 2024 11:05:27 +0700 Subject: [PATCH 06/17] [REFACTOR] Move admin API endpoint to seperate class --- .../controller/ReviewAdminController.java | 37 +++++++ .../review/controller/ReviewController.java | 21 +--- .../controller/ReviewAdminControllerTest.java | 100 ++++++++++++++++++ .../controller/ReviewControllerTest.java | 40 ------- 4 files changed, 138 insertions(+), 60 deletions(-) create mode 100644 src/main/java/snackscription/review/controller/ReviewAdminController.java create mode 100644 src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java diff --git a/src/main/java/snackscription/review/controller/ReviewAdminController.java b/src/main/java/snackscription/review/controller/ReviewAdminController.java new file mode 100644 index 0000000..073976b --- /dev/null +++ b/src/main/java/snackscription/review/controller/ReviewAdminController.java @@ -0,0 +1,37 @@ +package snackscription.review.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import snackscription.review.model.Review; +import snackscription.review.service.ReviewService; + +@RestController +@RequestMapping("/admin") +public class ReviewAdminController { + private ReviewService reviewService; + + public ReviewAdminController(ReviewService reviewService) { + this.reviewService = reviewService; + } + + @PutMapping("/subscription-boxes/{subsbox}/users/{user}/approve") + public ResponseEntity approveReview(@PathVariable String subsbox, @PathVariable String user) { + try { + Review review = reviewService.approveReview(subsbox, user); + return new ResponseEntity<>(review, HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } + + @PutMapping("/subscription-boxes/{subsbox}/users/{user}/reject") + public ResponseEntity rejectReview(@PathVariable String subsbox, @PathVariable String user) { + try { + Review review = reviewService.rejectReview(subsbox, user); + return new ResponseEntity<>(review, HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } +} diff --git a/src/main/java/snackscription/review/controller/ReviewController.java b/src/main/java/snackscription/review/controller/ReviewController.java index bd802cd..2825a5e 100644 --- a/src/main/java/snackscription/review/controller/ReviewController.java +++ b/src/main/java/snackscription/review/controller/ReviewController.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -98,24 +99,4 @@ public ResponseEntity deleteReview(@PathVariable String subsbox, @PathVa public List getSubsboxReview(@PathVariable String subsbox) throws Exception { return reviewService.getSubsboxReview(subsbox, null); } - - @PutMapping("/subscription-boxes/{subsbox}/users/{user}/approve") - public ResponseEntity approveReview(@PathVariable String subsbox, @PathVariable String user) { - try { - Review review = reviewService.approveReview(subsbox, user); - return new ResponseEntity<>(review, HttpStatus.OK); - } catch (Exception e) { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); - } - } - - @PutMapping("/subscription-boxes/{subsbox}/users/{user}/reject") - public ResponseEntity rejectReview(@PathVariable String subsbox, @PathVariable String user) { - try { - Review review = reviewService.rejectReview(subsbox, user); - return new ResponseEntity<>(review, HttpStatus.OK); - } catch (Exception e) { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); - } - } } \ No newline at end of file diff --git a/src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java b/src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java new file mode 100644 index 0000000..69fb75e --- /dev/null +++ b/src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java @@ -0,0 +1,100 @@ +package snackscription.review.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import org.springframework.test.web.servlet.ResultActions; +import snackscription.review.model.Review; +import snackscription.review.model.ReviewState; +import snackscription.review.service.ReviewService; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +@WebMvcTest(ReviewAdminController.class) +public class ReviewAdminControllerTest { + @MockBean + ReviewService reviewService; + + @Autowired + MockMvc mockMvc; + + List reviews; + static final String BASE_URL = "/admin"; + + @BeforeEach + public void setUp() { + this.reviews = new ArrayList<>(); + + Review review1 = new Review(5, "I love it", "subsbox_123", "user_123"); + Review review2 = new Review(1, "I hate it", "subsbox_123", "user_124"); + Review review3 = new Review(2, "Hmmmm idk", "subsbox_124", "user_124"); + Review review4 = new Review(3, "It's okay", "subsbox_124", "user_125"); + Review review5 = new Review(4, "I like it", "subsbox_124", "user_126"); + + review1.setState(ReviewState.PENDING); + review4.setState(ReviewState.APPROVED); + review5.setState(ReviewState.REJECTED); + + reviews = new ArrayList<>(); + reviews.add(review1); + reviews.add(review2); + reviews.add(review3); + reviews.add(review4); + reviews.add(review5); + } + + @Test + public void testApproveReview() throws Exception { + Review review = reviews.getFirst(); + + Review approvedReview = new Review(review.getRating(), review.getContent(), review.getSubsbox(), review.getAuthor()); + approvedReview.setState(ReviewState.APPROVED); + + when(reviewService.approveReview(review.getSubsbox(), review.getAuthor())).thenReturn(approvedReview); + + ResultActions result = mockMvc.perform(put(BASE_URL + "/subscription-boxes/{subsboxId}/users/{userId}/approve", review.getSubsbox(), review.getAuthor())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.rating", is(review.getRating()))) + .andExpect(jsonPath("$.content", is(review.getContent()))) + .andExpect(jsonPath("$.author", is(review.getAuthor()))) + .andExpect(jsonPath("$.subsbox", is(review.getSubsbox()))) + .andExpect(jsonPath("$.state", is("APPROVED"))); + + verify(reviewService).approveReview(review.getSubsbox(), review.getAuthor()); + } + + @Test + public void testRejectReview() throws Exception { + Review review = reviews.getFirst(); + + Review rejectedReview = new Review(review.getRating(), review.getContent(), review.getSubsbox(), review.getAuthor()); + rejectedReview.setState(ReviewState.REJECTED); + + when(reviewService.rejectReview(review.getSubsbox(), review.getAuthor())).thenReturn(rejectedReview); + + ResultActions result = mockMvc.perform(put(BASE_URL + "/subscription-boxes/{subsboxId}/users/{userId}/reject", review.getSubsbox(), review.getAuthor())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.rating", is(review.getRating()))) + .andExpect(jsonPath("$.content", is(review.getContent()))) + .andExpect(jsonPath("$.author", is(review.getAuthor()))) + .andExpect(jsonPath("$.subsbox", is(review.getSubsbox()))) + .andExpect(jsonPath("$.state", is("REJECTED"))); + + verify(reviewService).rejectReview(review.getSubsbox(), review.getAuthor()); + } +} diff --git a/src/test/java/snackscription/review/controller/ReviewControllerTest.java b/src/test/java/snackscription/review/controller/ReviewControllerTest.java index bf0ccca..9a08858 100644 --- a/src/test/java/snackscription/review/controller/ReviewControllerTest.java +++ b/src/test/java/snackscription/review/controller/ReviewControllerTest.java @@ -199,46 +199,6 @@ public void testDeleteUserSubscriptionBoxReview() throws Exception { verify(reviewService).deleteReview(subsbox, author); } - @Test - public void testApproveReview() throws Exception { - Review review = reviews.getFirst(); - - Review approvedReview = new Review(review.getRating(), review.getContent(), review.getSubsbox(), review.getAuthor()); - approvedReview.setState(ReviewState.APPROVED); - - when(reviewService.approveReview(review.getSubsbox(), review.getAuthor())).thenReturn(approvedReview); - - ResultActions result = mockMvc.perform(put("/reviews/subscription-boxes/{subsbox}/users/{author}/approve", review.getSubsbox(), review.getAuthor())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.rating", is(review.getRating()))) - .andExpect(jsonPath("$.content", is(review.getContent()))) - .andExpect(jsonPath("$.author", is(review.getAuthor()))) - .andExpect(jsonPath("$.subsbox", is(review.getSubsbox()))) - .andExpect(jsonPath("$.state", is("APPROVED"))); - - verify(reviewService).approveReview(review.getSubsbox(), review.getAuthor()); - } - - @Test - public void testRejectReview() throws Exception { - Review review = reviews.getFirst(); - - Review approvedReview = new Review(review.getRating(), review.getContent(), review.getSubsbox(), review.getAuthor()); - approvedReview.setState(ReviewState.REJECTED); - - when(reviewService.approveReview(review.getSubsbox(), review.getAuthor())).thenReturn(approvedReview); - - ResultActions result = mockMvc.perform(put("/reviews/subscription-boxes/{subsbox}/users/{author}/approve", review.getSubsbox(), review.getAuthor())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.rating", is(review.getRating()))) - .andExpect(jsonPath("$.content", is(review.getContent()))) - .andExpect(jsonPath("$.author", is(review.getAuthor()))) - .andExpect(jsonPath("$.subsbox", is(review.getSubsbox()))) - .andExpect(jsonPath("$.state", is("REJECTED"))); - - verify(reviewService).approveReview(review.getSubsbox(), review.getAuthor()); - } - // @Test // public void testGetAllSubscriptionBoxReview() { // String subsboxId = "subsboxId"; From 18834fee84e3ff355654621593ee9ec57b62c333 Mon Sep 17 00:00:00 2001 From: asteriskzie Date: Mon, 20 May 2024 11:13:20 +0700 Subject: [PATCH 07/17] [REFACTOR] Simplify public API endpoint path --- .../review/controller/ReviewController.java | 2 +- .../review/controller/ReviewControllerTest.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/snackscription/review/controller/ReviewController.java b/src/main/java/snackscription/review/controller/ReviewController.java index 2825a5e..8fe5a17 100644 --- a/src/main/java/snackscription/review/controller/ReviewController.java +++ b/src/main/java/snackscription/review/controller/ReviewController.java @@ -12,7 +12,7 @@ import snackscription.review.service.ReviewService; @RestController -@RequestMapping("/reviews") +@RequestMapping("") public class ReviewController { private ReviewService reviewService; diff --git a/src/test/java/snackscription/review/controller/ReviewControllerTest.java b/src/test/java/snackscription/review/controller/ReviewControllerTest.java index 9a08858..88e9e41 100644 --- a/src/test/java/snackscription/review/controller/ReviewControllerTest.java +++ b/src/test/java/snackscription/review/controller/ReviewControllerTest.java @@ -69,7 +69,7 @@ public void testCreateSubsboxReview() throws Exception{ when(reviewService.createReview(review.getRating(), review.getContent(), review.getId().getSubsbox(), review.getId().getAuthor())).thenReturn(review); - ResultActions result = mockMvc.perform(post("/reviews/subscription-boxes/{subsbox}", review.getSubsbox()) + ResultActions result = mockMvc.perform(post("/subscription-boxes/{subsbox}", review.getSubsbox()) .contentType(MediaType.APPLICATION_JSON) .content("{\"rating\": 5, \"content\": \"I love it\", \"author\": \"user_123\"}")) .andExpect(status().isCreated()) @@ -82,7 +82,7 @@ public void testCreateSubsboxReview() throws Exception{ } @Test - public void testReadAllPublicSubscriptionBoxReview() throws Exception { + public void testReadAllPublicSubsboxReviews() throws Exception { List approvedReviews = new ArrayList<>(); String subsbox = "subsbox_124"; for (Review review : reviews) { @@ -93,7 +93,7 @@ public void testReadAllPublicSubscriptionBoxReview() throws Exception { when(reviewService.getSubsboxReview(subsbox, "APPROVED")).thenReturn(approvedReviews); - String result = mockMvc.perform(get("/reviews/subscription-boxes/{subsbox}/public", subsbox)) + String result = mockMvc.perform(get("/subscription-boxes/{subsbox}/public", subsbox)) .andExpect(status().isOk()) .andExpect(jsonPath("$", hasSize(approvedReviews.size()))) .andReturn() @@ -134,7 +134,7 @@ public void readSelfSubscriptionBoxReview() throws Exception { when(reviewService.getReview(subsbox, author)).thenReturn(review); - ResultActions result = mockMvc.perform(get("/reviews/subscription-boxes/{subscriptionBoxId}/users/{author}", subsbox, author) + ResultActions result = mockMvc.perform(get("/subscription-boxes/{subscriptionBoxId}/users/{author}", subsbox, author) .contentType(MediaType.APPLICATION_JSON) .content("{\"author\": \"user_123\"}")) .andExpect(status().isOk()) @@ -192,7 +192,7 @@ public void testDeleteUserSubscriptionBoxReview() throws Exception { doNothing().when(reviewService).deleteReview(subsbox, author); - ResultActions result = mockMvc.perform(delete("/reviews/subscription-boxes/{subsbox}/users/{author}", subsbox, author) + ResultActions result = mockMvc.perform(delete("/subscription-boxes/{subsbox}/users/{author}", subsbox, author) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNoContent()); From 17ab860789e0e0c51660ea7f59ae3a73bf67074c Mon Sep 17 00:00:00 2001 From: asteriskzie Date: Mon, 20 May 2024 11:57:24 +0700 Subject: [PATCH 08/17] [RED] Add unhappy path test to review service --- .../review/service/ReviewServiceTest.java | 81 ++++++++++++++++++- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/src/test/java/snackscription/review/service/ReviewServiceTest.java b/src/test/java/snackscription/review/service/ReviewServiceTest.java index 6350528..071bf5c 100644 --- a/src/test/java/snackscription/review/service/ReviewServiceTest.java +++ b/src/test/java/snackscription/review/service/ReviewServiceTest.java @@ -18,6 +18,7 @@ import snackscription.review.exception.ReviewNotFoundException; import snackscription.review.model.Review; +import snackscription.review.model.ReviewId; import snackscription.review.model.ReviewState; import snackscription.review.repository.ReviewRepository; @@ -55,8 +56,6 @@ public void setUp() { @Test public void getReviewsBySubscriptionBoxId() throws Exception { - ReviewService reviewService = new ReviewService(reviewRepo); - List curReviews = new ArrayList<>(); String subscriptionBoxId = this.reviews.getFirst().getSubsbox(); @@ -75,10 +74,15 @@ public void getReviewsBySubscriptionBoxId() throws Exception { verify(reviewRepo).findByIdSubsbox(subscriptionBoxId); } + @Test + public void getReviewBySubsboxIdNotFound() throws Exception { + + } + @Test public void testCreateReview() throws Exception { Review review = reviews.getFirst(); - + when(reviewRepo.save(any(Review.class))).thenReturn(review); Review savedReview = reviewService.createReview( @@ -93,7 +97,37 @@ public void testCreateReview() throws Exception { } @Test - public void testgetSubsboxReview() throws Exception { + public void testCreateReviewUserNotFound() throws Exception { + + } + + @Test + public void testCreateReviewSubsboxNotFound() throws Exception { + + } + + @Test + public void testCreateReviewAlreadyExist() throws Exception { + Review review = reviews.get(0); + when(reviewRepo.existsById(review.getId())).thenReturn(true); + + assertThrows(Exception.class, () -> { + reviewService.createReview(review.getRating(), review.getContent(), review.getSubsbox(), review.getAuthor()); + }); + + verify(reviewRepo).existsById(review.getId()); + } + + @Test + public void testCreateReviewInvalidRating() throws Exception { + Review review = reviews.get(0); + assertThrows(Exception.class, () -> { + reviewService.createReview(-1, review.getContent(), review.getSubsbox(), review.getAuthor()); + }); + } + + @Test + public void testGetSubsboxReview() throws Exception { String subscriptionBoxId = this.reviews.getFirst().getSubsbox(); List curReviews = new ArrayList<>(); @@ -157,6 +191,29 @@ public void testEditReview() throws Exception { assertEquals(review.getId(), editedReview.getId()); } + @Test + public void testEditReviewNotFound() throws Exception { + Review review = reviews.getFirst(); + String subsbox = review.getSubsbox(); + String author = review.getAuthor(); + + int newRating = 1; + String newContent = "Changed content"; + Review newReview = new Review(newRating, newContent, author, subsbox); + newReview.setId(review.getId()); + + when(reviewRepo.findByIdSubsboxAndIdAuthor(subsbox, author)).thenReturn(review); + when(reviewRepo.save(any(Review.class))).thenReturn(newReview); + + Review editedReview = reviewService.editReview(newRating, newContent, subsbox, author); + + assertEquals(newRating, editedReview.getRating()); + assertEquals(newContent, editedReview.getContent()); + assertEquals(subsbox, editedReview.getSubsbox()); + assertEquals(author, editedReview.getAuthor()); + assertEquals(review.getId(), editedReview.getId()); + } + @Test public void testDeleteReview() throws Exception { String subsbox = this.reviews.getFirst().getSubsbox(); @@ -173,6 +230,22 @@ public void testDeleteReview() throws Exception { verify(reviewRepo).delete(review); } + @Test + public void testDeleteReviewNotFound() throws Exception { + String subsbox = this.reviews.getFirst().getSubsbox(); + String author = this.reviews.getFirst().getAuthor(); + + Review review = reviews.getFirst(); + + when(reviewRepo.findByIdSubsboxAndIdAuthor(subsbox, author)).thenReturn(review); + + reviewService.deleteReview(subsbox, author); + + assertThrows(ReviewNotFoundException.class, () -> reviewService.getReview(subsbox, author)); + + verify(reviewRepo).delete(review); + } + public void assertEqualReview(Review review1, Review review2) { assertEquals(review1.getRating(), review2.getRating()); assertEquals(review1.getContent(), review2.getContent()); From 7588297ffc19530cfbeebab1a45f2bd5884d265a Mon Sep 17 00:00:00 2001 From: asteriskzie Date: Mon, 20 May 2024 11:57:45 +0700 Subject: [PATCH 09/17] [GREEN] Handle unhappy path logic, add exceptions and such --- .../review/service/ReviewService.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/snackscription/review/service/ReviewService.java b/src/main/java/snackscription/review/service/ReviewService.java index 31a94c5..0e77afe 100644 --- a/src/main/java/snackscription/review/service/ReviewService.java +++ b/src/main/java/snackscription/review/service/ReviewService.java @@ -18,13 +18,35 @@ @Service @Component public class ReviewService { - private ReviewRepository reviewRepository; + private final ReviewRepository reviewRepository; public ReviewService (ReviewRepository reviewRepository) { this.reviewRepository = reviewRepository; } + public boolean reviewExist(String subsbox, String user) { + return reviewRepository.existsById(new ReviewId(subsbox, user)); + } + + public boolean subsboxExist(String subsbox) { + //TODO: connect to subsbox service + return true; + } + + public boolean userExist(String user) { + //TODO: connect to user service + return true; + } + public Review createReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception { + if (reviewExist(subscriptionBoxId, userId)) { + throw new Exception("User has made a review for this subscription box."); + } + + if (rating < 1 || rating > 5) { + throw new Exception("Rating out of range"); + } + Review review = new Review(rating, content, subscriptionBoxId, userId); reviewRepository.save(review); return review; @@ -39,6 +61,8 @@ public Review getReview(String subsbox, String user) throws Exception { } public List getSubsboxReview(String subscriptionBoxId, String state) throws Exception { + if (!subsboxExist(subscriptionBoxId)) throw new Exception(); + if (state == null) { return reviewRepository.findByIdSubsbox(subscriptionBoxId); } else { From 0dd6174c4ff5fbdf3a12bc10768718bd919139cf Mon Sep 17 00:00:00 2001 From: asteriskzie Date: Mon, 20 May 2024 12:18:44 +0700 Subject: [PATCH 10/17] [RED] Add test for admin controller get pending review --- .../controller/ReviewAdminControllerTest.java | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java b/src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java index 69fb75e..7e7596a 100644 --- a/src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java +++ b/src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java @@ -1,5 +1,6 @@ package snackscription.review.controller; +import com.jayway.jsonpath.JsonPath; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,11 +16,15 @@ import snackscription.review.service.ReviewService; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.verify; 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.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -46,7 +51,6 @@ public void setUp() { Review review4 = new Review(3, "It's okay", "subsbox_124", "user_125"); Review review5 = new Review(4, "I like it", "subsbox_124", "user_126"); - review1.setState(ReviewState.PENDING); review4.setState(ReviewState.APPROVED); review5.setState(ReviewState.REJECTED); @@ -97,4 +101,49 @@ public void testRejectReview() throws Exception { verify(reviewService).rejectReview(review.getSubsbox(), review.getAuthor()); } + + @Test + public void testGetAllPendingReview() throws Exception { + List pendingReviews = new ArrayList<>(); + String subsboxId = "subsbox_124"; + + for (Review review : reviews) { + if (review.getSubsbox().equals(subsboxId) && review.getState().equals(ReviewState.PENDING)) pendingReviews.add(review); + } + + when(reviewService.getSubsboxReview(subsboxId, ReviewState.PENDING.toString())).thenReturn(pendingReviews); + + String result = mockMvc.perform( + get(BASE_URL + "/subscription-boxes/{subsboxId}/reviews?state=PENDING", subsboxId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(pendingReviews.size()))) + .andReturn().getResponse().getContentAsString(); + + List foundReviews = new ArrayList(); + for (int i=0; i cmp = Comparator.comparing(Review::getAuthor); + pendingReviews.sort(cmp); + foundReviews.sort(cmp); + + for (int i=0; i Date: Mon, 20 May 2024 12:19:01 +0700 Subject: [PATCH 11/17] [GREEN] Implement admin controller get review by state --- .../review/controller/ReviewAdminController.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/snackscription/review/controller/ReviewAdminController.java b/src/main/java/snackscription/review/controller/ReviewAdminController.java index 073976b..4d736b7 100644 --- a/src/main/java/snackscription/review/controller/ReviewAdminController.java +++ b/src/main/java/snackscription/review/controller/ReviewAdminController.java @@ -6,6 +6,8 @@ import snackscription.review.model.Review; import snackscription.review.service.ReviewService; +import java.util.List; + @RestController @RequestMapping("/admin") public class ReviewAdminController { @@ -34,4 +36,13 @@ public ResponseEntity rejectReview(@PathVariable String subsbox, @PathVa return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } + + @GetMapping("/subscription-boxes/{subsbox}/reviews") + public ResponseEntity> getSubsboxReviews( + @PathVariable String subsbox, + @RequestParam(required = false) String state) throws Exception { + List reviews = reviewService.getSubsboxReview(subsbox, state); + return ResponseEntity.ok().body(reviews); + } + } From 0f133d56de72193042f1825109c6388d0adaf5b1 Mon Sep 17 00:00:00 2001 From: asteriskzie Date: Thu, 23 May 2024 15:18:56 +0700 Subject: [PATCH 12/17] [FIX] Fix getReview service --- .../review/controller/ReviewAdminController.java | 1 + .../review/controller/ReviewController.java | 8 ++------ .../java/snackscription/review/service/ReviewService.java | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/snackscription/review/controller/ReviewAdminController.java b/src/main/java/snackscription/review/controller/ReviewAdminController.java index 4d736b7..6d84d87 100644 --- a/src/main/java/snackscription/review/controller/ReviewAdminController.java +++ b/src/main/java/snackscription/review/controller/ReviewAdminController.java @@ -8,6 +8,7 @@ import java.util.List; +@CrossOrigin @RestController @RequestMapping("/admin") public class ReviewAdminController { diff --git a/src/main/java/snackscription/review/controller/ReviewController.java b/src/main/java/snackscription/review/controller/ReviewController.java index 8fe5a17..8e7cd01 100644 --- a/src/main/java/snackscription/review/controller/ReviewController.java +++ b/src/main/java/snackscription/review/controller/ReviewController.java @@ -11,6 +11,7 @@ import snackscription.review.model.Review; import snackscription.review.service.ReviewService; +@CrossOrigin @RestController @RequestMapping("") public class ReviewController { @@ -39,7 +40,7 @@ public ResponseEntity createSubsboxReview(@RequestBody Map> getPublicSubsboxReview(@PathVariable String subsbox) { try { List reviews = reviewService.getSubsboxReview(subsbox, "APPROVED"); @@ -94,9 +95,4 @@ public ResponseEntity deleteReview(@PathVariable String subsbox, @PathVa return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } - - @GetMapping("/subscription-boxes/{subsbox}") - public List getSubsboxReview(@PathVariable String subsbox) throws Exception { - return reviewService.getSubsboxReview(subsbox, null); - } } \ No newline at end of file diff --git a/src/main/java/snackscription/review/service/ReviewService.java b/src/main/java/snackscription/review/service/ReviewService.java index 0e77afe..687ae65 100644 --- a/src/main/java/snackscription/review/service/ReviewService.java +++ b/src/main/java/snackscription/review/service/ReviewService.java @@ -53,7 +53,7 @@ public Review createReview(int rating, String content, String subscriptionBoxId, } public Review getReview(String subsbox, String user) throws Exception { - Optional oreview = reviewRepository.findById(new ReviewId(user, subsbox)); + Optional oreview = reviewRepository.findById(new ReviewId(subsbox, user)); if (oreview.isEmpty()) { throw new ReviewNotFoundException(); } From bff470fd8ebb9075ff46fdabc66d7fec3ef23f92 Mon Sep 17 00:00:00 2001 From: asteriskzie Date: Thu, 23 May 2024 15:34:37 +0700 Subject: [PATCH 13/17] [RED] Add unhappy path test for review service --- .../review/repository/ReviewRepository.java | 10 ++ .../review/service/ReviewServiceTest.java | 97 +++++++------------ 2 files changed, 47 insertions(+), 60 deletions(-) diff --git a/src/main/java/snackscription/review/repository/ReviewRepository.java b/src/main/java/snackscription/review/repository/ReviewRepository.java index 096b317..f0f025e 100644 --- a/src/main/java/snackscription/review/repository/ReviewRepository.java +++ b/src/main/java/snackscription/review/repository/ReviewRepository.java @@ -2,14 +2,24 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; + +import io.micrometer.common.lang.Nullable; import snackscription.review.model.Review; import snackscription.review.model.ReviewId; import snackscription.review.model.ReviewState; public interface ReviewRepository extends JpaRepository { + @Nullable List findByIdSubsbox(String subsbox); + + @Nullable List findByIdAuthor(String author); + + @Nullable List findByIdSubsboxAndState(String subsbox, ReviewState state); + + @Nullable Review findByIdSubsboxAndIdAuthor(String subsbox, String author); + void deleteByIdSubsboxAndIdAuthor(String subsbox, String author); } \ No newline at end of file diff --git a/src/test/java/snackscription/review/service/ReviewServiceTest.java b/src/test/java/snackscription/review/service/ReviewServiceTest.java index 071bf5c..d15338e 100644 --- a/src/test/java/snackscription/review/service/ReviewServiceTest.java +++ b/src/test/java/snackscription/review/service/ReviewServiceTest.java @@ -1,7 +1,7 @@ package snackscription.review.service; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @@ -18,7 +18,6 @@ import snackscription.review.exception.ReviewNotFoundException; import snackscription.review.model.Review; -import snackscription.review.model.ReviewId; import snackscription.review.model.ReviewState; import snackscription.review.repository.ReviewRepository; @@ -54,11 +53,11 @@ public void setUp() { reviews.add(review5); } - @Test - public void getReviewsBySubscriptionBoxId() throws Exception { - List curReviews = new ArrayList<>(); + @Test + public void testGetSubsboxReview() throws Exception { String subscriptionBoxId = this.reviews.getFirst().getSubsbox(); + List curReviews = new ArrayList<>(); for (Review review : this.reviews) { if (review.getSubsbox().equals(subscriptionBoxId)) { curReviews.add(review); @@ -75,8 +74,38 @@ public void getReviewsBySubscriptionBoxId() throws Exception { } @Test - public void getReviewBySubsboxIdNotFound() throws Exception { + public void testGetSubsboxReviewNotFound() throws Exception { + String subscriptionBoxId = "nonexistent_subsbox_id"; + + when(reviewRepo.findByIdSubsbox(subscriptionBoxId)).thenReturn(null); + List foundReviews = reviewService.getSubsboxReview(subscriptionBoxId, null); + + assertNotNull(foundReviews); + assertEquals(0, foundReviews.size()); + + verify(reviewRepo).findByIdSubsbox(subscriptionBoxId); + } + + @Test + public void testGetSubsboxReviewApproved() throws Exception { + String subscriptionBoxId = this.reviews.getFirst().getSubsbox(); + + List cuReviews = new ArrayList<>(); + + for (Review review : this.reviews) { + if (review.getSubsbox().equals(subscriptionBoxId) && review.getState().equals(ReviewState.APPROVED)) { + cuReviews.add(review); + } + } + + when(reviewRepo.findByIdSubsboxAndState(subscriptionBoxId, ReviewState.APPROVED)).thenReturn(cuReviews); + + List foundReviews = reviewService.getSubsboxReview(subscriptionBoxId, "APPROVED"); + + assertEquals(cuReviews, foundReviews); + + verify(reviewRepo).findByIdSubsboxAndState(subscriptionBoxId, ReviewState.APPROVED); } @Test @@ -96,16 +125,6 @@ public void testCreateReview() throws Exception { verify(reviewRepo).save(any(Review.class)); } - @Test - public void testCreateReviewUserNotFound() throws Exception { - - } - - @Test - public void testCreateReviewSubsboxNotFound() throws Exception { - - } - @Test public void testCreateReviewAlreadyExist() throws Exception { Review review = reviews.get(0); @@ -126,48 +145,6 @@ public void testCreateReviewInvalidRating() throws Exception { }); } - @Test - public void testGetSubsboxReview() throws Exception { - String subscriptionBoxId = this.reviews.getFirst().getSubsbox(); - - List curReviews = new ArrayList<>(); - - for (Review review : this.reviews) { - if (review.getSubsbox().equals(subscriptionBoxId)) { - curReviews.add(review); - } - } - - when(reviewRepo.findByIdSubsbox(subscriptionBoxId)).thenReturn(curReviews); - - List foundReviews = reviewService.getSubsboxReview(subscriptionBoxId, null); - - assertEquals(curReviews, foundReviews); - - verify(reviewRepo).findByIdSubsbox(subscriptionBoxId); - } - - @Test - public void testgetSubsboxReviewApproved() throws Exception { - String subscriptionBoxId = this.reviews.getFirst().getSubsbox(); - - List cuReviews = new ArrayList<>(); - - for (Review review : this.reviews) { - if (review.getSubsbox().equals(subscriptionBoxId) && review.getState().equals(ReviewState.APPROVED)) { - cuReviews.add(review); - } - } - - when(reviewRepo.findByIdSubsboxAndState(subscriptionBoxId, ReviewState.APPROVED)).thenReturn(cuReviews); - - List foundReviews = reviewService.getSubsboxReview(subscriptionBoxId, "APPROVED"); - - assertEquals(cuReviews, foundReviews); - - verify(reviewRepo).findByIdSubsboxAndState(subscriptionBoxId, ReviewState.APPROVED); - } - @Test public void testEditReview() throws Exception { Review review = reviews.getFirst(); @@ -202,7 +179,7 @@ public void testEditReviewNotFound() throws Exception { Review newReview = new Review(newRating, newContent, author, subsbox); newReview.setId(review.getId()); - when(reviewRepo.findByIdSubsboxAndIdAuthor(subsbox, author)).thenReturn(review); + when(reviewRepo.findByIdSubsboxAndIdAuthor(subsbox, author)).thenReturn(null); when(reviewRepo.save(any(Review.class))).thenReturn(newReview); Review editedReview = reviewService.editReview(newRating, newContent, subsbox, author); @@ -237,7 +214,7 @@ public void testDeleteReviewNotFound() throws Exception { Review review = reviews.getFirst(); - when(reviewRepo.findByIdSubsboxAndIdAuthor(subsbox, author)).thenReturn(review); + when(reviewRepo.findByIdSubsboxAndIdAuthor(subsbox, author)).thenReturn(null); reviewService.deleteReview(subsbox, author); From e728a5a614eb4322ce94a192365ece43aecf16be Mon Sep 17 00:00:00 2001 From: asteriskzie Date: Thu, 23 May 2024 15:47:29 +0700 Subject: [PATCH 14/17] [GREEN] Fix review service logic based on unhappy path test --- .../review/service/ReviewService.java | 29 ++++++++----------- .../review/service/ReviewServiceTest.java | 27 ++++++++--------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/main/java/snackscription/review/service/ReviewService.java b/src/main/java/snackscription/review/service/ReviewService.java index 687ae65..25d163a 100644 --- a/src/main/java/snackscription/review/service/ReviewService.java +++ b/src/main/java/snackscription/review/service/ReviewService.java @@ -1,5 +1,6 @@ package snackscription.review.service; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; @@ -28,16 +29,6 @@ public boolean reviewExist(String subsbox, String user) { return reviewRepository.existsById(new ReviewId(subsbox, user)); } - public boolean subsboxExist(String subsbox) { - //TODO: connect to subsbox service - return true; - } - - public boolean userExist(String user) { - //TODO: connect to user service - return true; - } - public Review createReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception { if (reviewExist(subscriptionBoxId, userId)) { throw new Exception("User has made a review for this subscription box."); @@ -61,18 +52,22 @@ public Review getReview(String subsbox, String user) throws Exception { } public List getSubsboxReview(String subscriptionBoxId, String state) throws Exception { - if (!subsboxExist(subscriptionBoxId)) throw new Exception(); - + List result; if (state == null) { - return reviewRepository.findByIdSubsbox(subscriptionBoxId); + result = reviewRepository.findByIdSubsbox(subscriptionBoxId); } else { state = state.toUpperCase(); - ReviewState reviewState = Enum.valueOf(ReviewState.class, state); - if (reviewState == null) { + if (!state.equals("PENDING") && !state.equals("APPROVED") && !state.equals("REJECTED")) { throw new InvalidStateException(); } - return reviewRepository.findByIdSubsboxAndState(subscriptionBoxId, reviewState); - } + ReviewState reviewState = ReviewState.valueOf(state); + result = reviewRepository.findByIdSubsboxAndState(subscriptionBoxId, reviewState); + } + + if (result == null) { + result = new ArrayList<>(); + } + return result; } public Review editReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception { diff --git a/src/test/java/snackscription/review/service/ReviewServiceTest.java b/src/test/java/snackscription/review/service/ReviewServiceTest.java index d15338e..d1fbfe1 100644 --- a/src/test/java/snackscription/review/service/ReviewServiceTest.java +++ b/src/test/java/snackscription/review/service/ReviewServiceTest.java @@ -16,6 +16,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import snackscription.review.exception.InvalidStateException; import snackscription.review.exception.ReviewNotFoundException; import snackscription.review.model.Review; import snackscription.review.model.ReviewState; @@ -108,6 +109,15 @@ public void testGetSubsboxReviewApproved() throws Exception { verify(reviewRepo).findByIdSubsboxAndState(subscriptionBoxId, ReviewState.APPROVED); } + @Test + public void testGetSubsboxReviewInvalidState() throws Exception { + String subscriptionBoxId = this.reviews.getFirst().getSubsbox(); + + assertThrows(InvalidStateException.class, () -> { + reviewService.getSubsboxReview(subscriptionBoxId, "INVALID_STATE"); + }); + } + @Test public void testCreateReview() throws Exception { Review review = reviews.getFirst(); @@ -180,15 +190,10 @@ public void testEditReviewNotFound() throws Exception { newReview.setId(review.getId()); when(reviewRepo.findByIdSubsboxAndIdAuthor(subsbox, author)).thenReturn(null); - when(reviewRepo.save(any(Review.class))).thenReturn(newReview); - Review editedReview = reviewService.editReview(newRating, newContent, subsbox, author); - - assertEquals(newRating, editedReview.getRating()); - assertEquals(newContent, editedReview.getContent()); - assertEquals(subsbox, editedReview.getSubsbox()); - assertEquals(author, editedReview.getAuthor()); - assertEquals(review.getId(), editedReview.getId()); + assertThrows(ReviewNotFoundException.class, () -> { + reviewService.editReview(newRating, newContent, subsbox, author); + }); } @Test @@ -216,11 +221,7 @@ public void testDeleteReviewNotFound() throws Exception { when(reviewRepo.findByIdSubsboxAndIdAuthor(subsbox, author)).thenReturn(null); - reviewService.deleteReview(subsbox, author); - - assertThrows(ReviewNotFoundException.class, () -> reviewService.getReview(subsbox, author)); - - verify(reviewRepo).delete(review); + assertThrows(ReviewNotFoundException.class, () -> reviewService.deleteReview(subsbox, author)); } public void assertEqualReview(Review review1, Review review2) { From 5ae121458c5cb53bbaed0469c809a0e708e348db Mon Sep 17 00:00:00 2001 From: asteriskzie Date: Thu, 23 May 2024 15:51:34 +0700 Subject: [PATCH 15/17] [REFACTOR] Make interface for review service --- .../controller/ReviewAdminController.java | 6 +- .../review/controller/ReviewController.java | 7 +- .../review/service/ReviewService.java | 112 ++---------------- .../review/service/ReviewServiceImpl.java | 107 +++++++++++++++++ .../controller/ReviewAdminControllerTest.java | 4 +- .../review/service/ReviewServiceTest.java | 2 +- 6 files changed, 126 insertions(+), 112 deletions(-) create mode 100644 src/main/java/snackscription/review/service/ReviewServiceImpl.java diff --git a/src/main/java/snackscription/review/controller/ReviewAdminController.java b/src/main/java/snackscription/review/controller/ReviewAdminController.java index 6d84d87..25b7d0b 100644 --- a/src/main/java/snackscription/review/controller/ReviewAdminController.java +++ b/src/main/java/snackscription/review/controller/ReviewAdminController.java @@ -4,7 +4,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import snackscription.review.model.Review; -import snackscription.review.service.ReviewService; +import snackscription.review.service.ReviewServiceImpl; import java.util.List; @@ -12,9 +12,9 @@ @RestController @RequestMapping("/admin") public class ReviewAdminController { - private ReviewService reviewService; + private ReviewServiceImpl reviewService; - public ReviewAdminController(ReviewService reviewService) { + public ReviewAdminController(ReviewServiceImpl reviewService) { this.reviewService = reviewService; } diff --git a/src/main/java/snackscription/review/controller/ReviewController.java b/src/main/java/snackscription/review/controller/ReviewController.java index 8e7cd01..0165a1c 100644 --- a/src/main/java/snackscription/review/controller/ReviewController.java +++ b/src/main/java/snackscription/review/controller/ReviewController.java @@ -3,21 +3,20 @@ import java.util.List; import java.util.Map; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import snackscription.review.model.Review; -import snackscription.review.service.ReviewService; +import snackscription.review.service.ReviewServiceImpl; @CrossOrigin @RestController @RequestMapping("") public class ReviewController { - private ReviewService reviewService; + private ReviewServiceImpl reviewService; - public ReviewController(ReviewService reviewService) { + public ReviewController(ReviewServiceImpl reviewService) { this.reviewService = reviewService; } diff --git a/src/main/java/snackscription/review/service/ReviewService.java b/src/main/java/snackscription/review/service/ReviewService.java index 25d163a..aee0263 100644 --- a/src/main/java/snackscription/review/service/ReviewService.java +++ b/src/main/java/snackscription/review/service/ReviewService.java @@ -1,107 +1,15 @@ package snackscription.review.service; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; - -import io.micrometer.core.instrument.composite.CompositeMeterRegistry; -import org.springframework.stereotype.Component; -import org.springframework.stereotype.Service; - -import snackscription.review.exception.InvalidStateException; -import snackscription.review.exception.ReviewNotFoundException; import snackscription.review.model.Review; -import snackscription.review.model.ReviewId; -import snackscription.review.model.ReviewState; -import snackscription.review.repository.ReviewRepository; - -@Service -@Component -public class ReviewService { - private final ReviewRepository reviewRepository; - - public ReviewService (ReviewRepository reviewRepository) { - this.reviewRepository = reviewRepository; - } - - public boolean reviewExist(String subsbox, String user) { - return reviewRepository.existsById(new ReviewId(subsbox, user)); - } - - public Review createReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception { - if (reviewExist(subscriptionBoxId, userId)) { - throw new Exception("User has made a review for this subscription box."); - } - - if (rating < 1 || rating > 5) { - throw new Exception("Rating out of range"); - } - - Review review = new Review(rating, content, subscriptionBoxId, userId); - reviewRepository.save(review); - return review; - } - - public Review getReview(String subsbox, String user) throws Exception { - Optional oreview = reviewRepository.findById(new ReviewId(subsbox, user)); - if (oreview.isEmpty()) { - throw new ReviewNotFoundException(); - } - return oreview.get(); - } - - public List getSubsboxReview(String subscriptionBoxId, String state) throws Exception { - List result; - if (state == null) { - result = reviewRepository.findByIdSubsbox(subscriptionBoxId); - } else { - state = state.toUpperCase(); - if (!state.equals("PENDING") && !state.equals("APPROVED") && !state.equals("REJECTED")) { - throw new InvalidStateException(); - } - ReviewState reviewState = ReviewState.valueOf(state); - result = reviewRepository.findByIdSubsboxAndState(subscriptionBoxId, reviewState); - } - - if (result == null) { - result = new ArrayList<>(); - } - return result; - } - - public Review editReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception { - Review review = reviewRepository.findByIdSubsboxAndIdAuthor(subscriptionBoxId, userId); - - if (review == null) { - throw new ReviewNotFoundException(); - } - - review.setRating(rating); - review.setContent(content); - - return reviewRepository.save(review); - } - - public Review approveReview(String subsbox, String user) throws Exception { - Review review = getReview(subsbox, user); - review.approve(); - return reviewRepository.save(review); - } - - public Review rejectReview(String subsbox, String user) throws Exception { - Review review = getReview(subsbox, user); - review.reject(); - return reviewRepository.save(review); - } - - public void deleteReview(String subsbox, String user) throws Exception { - Review review = reviewRepository.findByIdSubsboxAndIdAuthor(subsbox, user); - - if (review == null) { - throw new ReviewNotFoundException(); - } +import java.util.List; - reviewRepository.delete(review); - } +public interface ReviewService { + public boolean reviewExist(String subsbox, String user); + public Review createReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception; + public Review getReview(String subsbox, String user) throws Exception; + public List getSubsboxReview(String subscriptionBoxId, String state) throws Exception; + public Review editReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception; + public void deleteReview(String subsbox, String user) throws Exception; + public Review approveReview(String subsbox, String user) throws Exception; + public Review rejectReview(String subsbox, String user) throws Exception; } diff --git a/src/main/java/snackscription/review/service/ReviewServiceImpl.java b/src/main/java/snackscription/review/service/ReviewServiceImpl.java new file mode 100644 index 0000000..4b0006c --- /dev/null +++ b/src/main/java/snackscription/review/service/ReviewServiceImpl.java @@ -0,0 +1,107 @@ +package snackscription.review.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +import snackscription.review.exception.InvalidStateException; +import snackscription.review.exception.ReviewNotFoundException; +import snackscription.review.model.Review; +import snackscription.review.model.ReviewId; +import snackscription.review.model.ReviewState; +import snackscription.review.repository.ReviewRepository; + +@Service +@Component +public class ReviewServiceImpl implements ReviewService { + private final ReviewRepository reviewRepository; + + public ReviewServiceImpl (ReviewRepository reviewRepository) { + this.reviewRepository = reviewRepository; + } + + public boolean reviewExist(String subsbox, String user) { + return reviewRepository.existsById(new ReviewId(subsbox, user)); + } + + public Review createReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception { + if (reviewExist(subscriptionBoxId, userId)) { + throw new Exception("User has made a review for this subscription box."); + } + + if (rating < 1 || rating > 5) { + throw new Exception("Rating out of range"); + } + + Review review = new Review(rating, content, subscriptionBoxId, userId); + reviewRepository.save(review); + return review; + } + + public Review getReview(String subsbox, String user) throws Exception { + Optional oreview = reviewRepository.findById(new ReviewId(subsbox, user)); + if (oreview.isEmpty()) { + throw new ReviewNotFoundException(); + } + return oreview.get(); + } + + public List getSubsboxReview(String subscriptionBoxId, String state) throws Exception { + List result; + if (state == null) { + result = reviewRepository.findByIdSubsbox(subscriptionBoxId); + } else { + state = state.toUpperCase(); + if (!state.equals("PENDING") && !state.equals("APPROVED") && !state.equals("REJECTED")) { + throw new InvalidStateException(); + } + ReviewState reviewState = ReviewState.valueOf(state); + result = reviewRepository.findByIdSubsboxAndState(subscriptionBoxId, reviewState); + } + + if (result == null) { + result = new ArrayList<>(); + } + return result; + } + + public Review editReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception { + Review review = reviewRepository.findByIdSubsboxAndIdAuthor(subscriptionBoxId, userId); + + if (review == null) { + throw new ReviewNotFoundException(); + } + + review.setRating(rating); + review.setContent(content); + + return reviewRepository.save(review); + } + + public Review approveReview(String subsbox, String user) throws Exception { + Review review = getReview(subsbox, user); + review.approve(); + return reviewRepository.save(review); + } + + public Review rejectReview(String subsbox, String user) throws Exception { + Review review = getReview(subsbox, user); + review.reject(); + return reviewRepository.save(review); + } + + public void deleteReview(String subsbox, String user) throws Exception { + Review review = reviewRepository.findByIdSubsboxAndIdAuthor(subsbox, user); + + if (review == null) { + throw new ReviewNotFoundException(); + } + + reviewRepository.delete(review); + } +} diff --git a/src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java b/src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java index 7e7596a..75cf3f4 100644 --- a/src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java +++ b/src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java @@ -13,7 +13,7 @@ import org.springframework.test.web.servlet.ResultActions; import snackscription.review.model.Review; import snackscription.review.model.ReviewState; -import snackscription.review.service.ReviewService; +import snackscription.review.service.ReviewServiceImpl; import java.util.ArrayList; import java.util.Comparator; @@ -33,7 +33,7 @@ @WebMvcTest(ReviewAdminController.class) public class ReviewAdminControllerTest { @MockBean - ReviewService reviewService; + ReviewServiceImpl reviewService; @Autowired MockMvc mockMvc; diff --git a/src/test/java/snackscription/review/service/ReviewServiceTest.java b/src/test/java/snackscription/review/service/ReviewServiceTest.java index d1fbfe1..3cacbff 100644 --- a/src/test/java/snackscription/review/service/ReviewServiceTest.java +++ b/src/test/java/snackscription/review/service/ReviewServiceTest.java @@ -34,7 +34,7 @@ public class ReviewServiceTest { @BeforeEach public void setUp() { - reviewService = new ReviewService(reviewRepo); + reviewService = new ReviewServiceImpl(reviewRepo); Review review1 = new Review(5, "I love it", "subsbox_123", "user_123"); Review review2 = new Review(1, "I hate it", "subsbox_123", "user_124"); From 47a7ff4d8ac91f5f0618300480338f59caeb3da3 Mon Sep 17 00:00:00 2001 From: asteriskzie Date: Sun, 26 May 2024 19:16:47 +0700 Subject: [PATCH 16/17] [REFACTOR] Fix API endpoints naming --- .../review/controller/ReviewController.java | 43 ++--- .../controller/ReviewControllerTest.java | 169 +++--------------- 2 files changed, 46 insertions(+), 166 deletions(-) diff --git a/src/main/java/snackscription/review/controller/ReviewController.java b/src/main/java/snackscription/review/controller/ReviewController.java index 0165a1c..6ce65b5 100644 --- a/src/main/java/snackscription/review/controller/ReviewController.java +++ b/src/main/java/snackscription/review/controller/ReviewController.java @@ -8,15 +8,16 @@ import org.springframework.web.bind.annotation.*; import snackscription.review.model.Review; +import snackscription.review.service.ReviewService; import snackscription.review.service.ReviewServiceImpl; @CrossOrigin @RestController @RequestMapping("") public class ReviewController { - private ReviewServiceImpl reviewService; + private ReviewService reviewService; - public ReviewController(ReviewServiceImpl reviewService) { + public ReviewController(ReviewService reviewService) { this.reviewService = reviewService; } @@ -25,8 +26,8 @@ public ResponseEntity reviewPage() { return ResponseEntity.ok().body("Welcome to the review service!"); } - @PostMapping("/subscription-boxes/{subsbox}") - public ResponseEntity createSubsboxReview(@RequestBody Map body, @PathVariable String subsbox) { + @PostMapping("/subscription-boxes/{subsbox}/users/self") + public ResponseEntity createSelfSubsboxReview(@RequestBody Map body, @PathVariable String subsbox) { try { String author = body.get("author"); int rating = Integer.parseInt(body.get("rating")); @@ -49,46 +50,36 @@ public ResponseEntity> getPublicSubsboxReview(@PathVariable String } } - @GetMapping("/subscription-boxes/{subsbox}/users/{user}") - public ResponseEntity getSelfSubsboxReview(@RequestBody Map body, @PathVariable String subsbox, @PathVariable String user) { + @GetMapping("/subscription-boxes/{subsbox}/users/self") + public ResponseEntity getSelfSubsboxReview(@RequestBody Map body, @PathVariable String subsbox) { try { - String sender = body.get("author"); // TODO: nanti pakai JWT token untuk ambil sendernya - if (!authenticate(sender, user)) { - return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); - } - Review review = reviewService.getReview(subsbox, user); + String author = body.get("author"); // TODO: nanti pakai JWT token untuk ambil sendernya + Review review = reviewService.getReview(subsbox, author); return new ResponseEntity<>(review, HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } - @PutMapping("/subscription-boxes/{subsbox}/users/{user}") - public ResponseEntity editReview(@RequestBody Map body, @PathVariable String subsbox, @PathVariable String user) { + @PutMapping("/subscription-boxes/{subsbox}/users/self") + public ResponseEntity editSelfReview(@RequestBody Map body, @PathVariable String subsbox) { try { - String sender = body.get("author"); // TODO: nanti pakai JWT token untuk ambil sendernya - if (!authenticate(sender, user)) { - return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); - } - + String author = body.get("author"); // TODO: nanti pakai JWT token untuk ambil sendernya int rating = Integer.parseInt(body.get("rating")); String content = body.get("content"); - Review review = reviewService.editReview(rating, content, subsbox, user); + Review review = reviewService.editReview(rating, content, subsbox, author); return new ResponseEntity<>(review, HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } - private boolean authenticate(String sender, String user) { - return true; - } - - @DeleteMapping("/subscription-boxes/{subsbox}/users/{user}") - public ResponseEntity deleteReview(@PathVariable String subsbox, @PathVariable String user) { + @DeleteMapping("/subscription-boxes/{subsbox}/users/self") + public ResponseEntity deleteSelfReview(@RequestBody Map body, @PathVariable String subsbox) { try { - reviewService.deleteReview(subsbox, user); + String author = body.get("author"); // TODO: nanti pakai JWT token untuk ambil sendernya + reviewService.deleteReview(subsbox, author); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } catch (Exception e) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); diff --git a/src/test/java/snackscription/review/controller/ReviewControllerTest.java b/src/test/java/snackscription/review/controller/ReviewControllerTest.java index 88e9e41..edf1599 100644 --- a/src/test/java/snackscription/review/controller/ReviewControllerTest.java +++ b/src/test/java/snackscription/review/controller/ReviewControllerTest.java @@ -69,7 +69,7 @@ public void testCreateSubsboxReview() throws Exception{ when(reviewService.createReview(review.getRating(), review.getContent(), review.getId().getSubsbox(), review.getId().getAuthor())).thenReturn(review); - ResultActions result = mockMvc.perform(post("/subscription-boxes/{subsbox}", review.getSubsbox()) + ResultActions result = mockMvc.perform(post("/subscription-boxes/{subsbox}/users/self", review.getSubsbox()) .contentType(MediaType.APPLICATION_JSON) .content("{\"rating\": 5, \"content\": \"I love it\", \"author\": \"user_123\"}")) .andExpect(status().isCreated()) @@ -93,7 +93,7 @@ public void testReadAllPublicSubsboxReviews() throws Exception { when(reviewService.getSubsboxReview(subsbox, "APPROVED")).thenReturn(approvedReviews); - String result = mockMvc.perform(get("/subscription-boxes/{subsbox}/public", subsbox)) + String result = mockMvc.perform(get("/subscription-boxes/{subsbox}", subsbox)) .andExpect(status().isOk()) .andExpect(jsonPath("$", hasSize(approvedReviews.size()))) .andReturn() @@ -127,14 +127,14 @@ public void testReadAllPublicSubsboxReviews() throws Exception { } @Test - public void readSelfSubscriptionBoxReview() throws Exception { + public void testReadSelfSubsboxReview() throws Exception { Review review = reviews.getFirst(); String subsbox = review.getSubsbox(); String author = review.getAuthor(); when(reviewService.getReview(subsbox, author)).thenReturn(review); - ResultActions result = mockMvc.perform(get("/subscription-boxes/{subscriptionBoxId}/users/{author}", subsbox, author) + ResultActions result = mockMvc.perform(get("/subscription-boxes/{subscriptionBoxId}/users/self", subsbox, author) .contentType(MediaType.APPLICATION_JSON) .content("{\"author\": \"user_123\"}")) .andExpect(status().isOk()) @@ -146,152 +146,41 @@ public void readSelfSubscriptionBoxReview() throws Exception { verify(reviewService).getReview(subsbox, author); } -// @Test -// public void testEditSelfSubscriptionBoxReview() throws Exception { -// Review review = reviews.getFirst(); -// String subsboxId = review.getSubsbox(); -// String userId = review.getAuthor(); -// -// int newRating = 4; -// String newContent = "Awikwok"; -// when(reviewService.editReview(newRating, newContent, subsboxId, userId)).thenReturn(new Review(newRating, newContent, userId, subsboxId)); -// -// ResultActions result = mockMvc.perform(put("/reviews/subscription-boxes/{subscriptionBoxId}/users/self", subsboxId) -// .contentType(MediaType.APPLICATION_JSON) -// .content("{\"rating\": 4, \"content\": \"Awikwok\", \"userId\": \"user_123\"}")) -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$.rating", is(newRating))) -// .andExpect(jsonPath("$.content", is(newContent))) -// .andExpect(jsonPath("$.userId", is(review.getAuthor()))) -// .andExpect(jsonPath("$.subscriptionBoxId", is(review.getSubsbox()))); -// -// verify(reviewService).editReview(newRating, newContent, subsboxId, userId); -// } -// -// @Test -// public void testDeleteSelfSubscriptionBoxReview() throws Exception { -// Review review = reviews.getFirst(); -// String subsboxId = review.getSubsbox(); -// String userId = review.getAuthor(); -// -// doNothing().when(reviewService).deleteReview(subsboxId, userId); -// -// ResultActions result = mockMvc.perform(delete("/subscription-boxes/{subscriptionBoxId}/users/self", subsboxId) -// .contentType(MediaType.APPLICATION_JSON) -// .content("{\"userId\": \"user_123\"}")) -// .andExpect(status().isNoContent()); -// -// verify(reviewService).deleteReview(subsboxId, userId); -// } + @Test + public void testEditSelfSubsboxReview() throws Exception { + Review review = reviews.getFirst(); + String subsboxId = review.getSubsbox(); + String userId = review.getAuthor(); + + int newRating = 4; + String newContent = "Awikwok"; + when(reviewService.editReview(newRating, newContent, subsboxId, userId)).thenReturn(new Review(newRating, newContent, subsboxId, userId)); + + ResultActions result = mockMvc.perform(put("/subscription-boxes/{subscriptionBoxId}/users/self", subsboxId) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"rating\": 4, \"content\": \"Awikwok\", \"author\": \"user_123\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.rating", is(newRating))) + .andExpect(jsonPath("$.content", is(newContent))) + .andExpect(jsonPath("$.author", is(review.getAuthor()))) + .andExpect(jsonPath("$.subsbox", is(review.getSubsbox()))); + + verify(reviewService).editReview(newRating, newContent, subsboxId, userId); + } @Test - public void testDeleteUserSubscriptionBoxReview() throws Exception { + public void testDeleteSelfSubsboxReview() throws Exception { Review review = reviews.getFirst(); String subsbox = review.getSubsbox(); String author = review.getAuthor(); doNothing().when(reviewService).deleteReview(subsbox, author); - ResultActions result = mockMvc.perform(delete("/subscription-boxes/{subsbox}/users/{author}", subsbox, author) - .contentType(MediaType.APPLICATION_JSON)) + ResultActions result = mockMvc.perform(delete("/subscription-boxes/{subsbox}/users/self", subsbox) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"author\": \"user_123\"}")) .andExpect(status().isNoContent()); verify(reviewService).deleteReview(subsbox, author); } - -// @Test -// public void testGetAllSubscriptionBoxReview() { -// String subsboxId = "subsboxId"; - -// ArrayList reviews = new ArrayList<>(); -// reviews.add(new Review(5, "amazing", "user1", subsboxId)); -// reviews.add(new Review(4, "good", "user2", subsboxId)); - -// when(reviewService.testGetAllSubscriptionBoxReview(subsboxId)).thenReturn(reviews); - -// ResultActions result = mockMvc.perform(get("/api/subscription-boxes/{subsboxId}", subsboxId)) -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$", hasSize(2))) -// .andExpect(jsonPath("$[0].rating", is(5))) -// .andExpect(jsonPath("$[0].content", is("amazing"))) -// .andExpect(jsonPath("$[0].userId", is("user1"))) -// .andExpect(jsonPath("$[0].subscriptionBoxId", is(subsboxId))) -// .andExpect(jsonPath("$[1].rating", is(4))) -// .andExpect(jsonPath("$[1].content", is("good"))) -// .andExpect(jsonPath("$[1].userId", is("user2"))) -// .andExpect(jsonPath("$[1].subscriptionBoxId", is(subsboxId))); - -// verify(reviewService).testGetAllSubscriptionBoxReview(subsboxId); -// } - -// @Test -// public void testGetById() throws Exception { -// Review review = new Review( -// 5, "amazing", "user1", "subsboxId" -// ); -// String reviewId = review.getId(); - -// when(reviewService.findById(reviewId)).thenReturn(review); - -// ResultActions result = mockMvc.perform(get("/api/reviews/{reviewId}", reviewId)) -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$.rating", is(5))) -// .andExpect(jsonPath("$.content", is("amazing"))) -// .andExpect(jsonPath("$.userId", is("user1"))) -// .andExpect(jsonPath("$.subscriptionBoxId", is("subsboxId"))); - -// verify(reviewService).findById(reviewId); -// } - -// @Test -// public void testGetBySubscriptionBoxId() throws Exception { -// List curReviews = new ArrayList<>(); - -// String subscriptionBoxId = this.reviews.getFirst().getSubsbox(); -// for (Review review : this.reviews) { -// if (review.getSubsbox().equals(subscriptionBoxId)) { -// curReviews.add(review); -// } -// } - -// when(reviewService.findBySubscriptionBoxId(subscriptionBoxId)).thenReturn(curReviews); - -// String result = mockMvc.perform(get("/api/subscription-boxes/{subscriptionBoxId}", subscriptionBoxId)) -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$", hasSize(curReviews.size()))) -// .andReturn() -// .getResponse() -// .getContentAsString(); - -// List foundReviews = new ArrayList(); -// for (int i=0; i cmp = new Comparator() { -// @Override -// public int compare(Review o1, Review o2) { -// return o1.getAuthor().compareTo(o2.getAuthor()); -// } -// }; - -// curReviews.sort(cmp); -// foundReviews.sort(cmp); - -// for (int i=0; i Date: Sun, 26 May 2024 20:37:06 +0700 Subject: [PATCH 17/17] merge branch 'dev' into 'async' --- .github/workflows/cd.yml | 22 +- .github/workflows/ci.yml | 13 +- .monitoring/docker-compose.yml | 19 ++ .../provisioning/datasources/datasources.yml | 7 + .monitoring/prometheus/prometheus.yml | 11 + application.yml | 5 + build.gradle.kts | 16 +- deployment.yaml | 22 ++ gradle/gradle.properties | 5 + gradlew | 2 +- service.yaml | 11 + .../review/ReviewAppConfig.java | 14 + .../review/ReviewApplication.java | 18 +- .../controller/ReviewAdminController.java | 49 +++ .../review/controller/ReviewController.java | 114 ++----- .../snackscription/review/model/Review.java | 34 +-- .../snackscription/review/model/ReviewId.java | 25 ++ .../review/repository/ReviewRepository.java | 24 +- .../review/service/ReviewService.java | 107 +------ .../review/service/ReviewServiceImpl.java | 117 ++++++++ .../service/SentimentAnalysisService.java | 5 + src/main/resources/application-dev.properties | 2 +- src/main/resources/application.properties | 4 +- .../controller/ReviewAdminControllerTest.java | 149 ++++++++++ .../controller/ReviewControllerTest.java | 280 ++++-------------- .../review/model/ReviewTest.java | 6 +- .../repository/ReviewRepositoryTest.java | 36 ++- .../review/service/ReviewServiceTest.java | 234 +++++++-------- 28 files changed, 730 insertions(+), 621 deletions(-) create mode 100644 .monitoring/docker-compose.yml create mode 100644 .monitoring/grafana/provisioning/datasources/datasources.yml create mode 100644 .monitoring/prometheus/prometheus.yml create mode 100644 application.yml create mode 100644 deployment.yaml create mode 100644 gradle/gradle.properties create mode 100644 service.yaml create mode 100644 src/main/java/snackscription/review/ReviewAppConfig.java create mode 100644 src/main/java/snackscription/review/controller/ReviewAdminController.java create mode 100644 src/main/java/snackscription/review/model/ReviewId.java create mode 100644 src/main/java/snackscription/review/service/ReviewServiceImpl.java create mode 100644 src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 1153188..30e9471 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -45,16 +45,6 @@ jobs: name: Test runs-on: ubuntu-latest needs: build - services: - postgres: - image: postgres:latest - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: snackscription_review - ports: - - 5432:5432 - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout Repository uses: actions/checkout@v4 @@ -80,12 +70,24 @@ jobs: ./gradlew check --info --stacktrace ./gradlew test ./gradlew jacocoTestReport + env: + PRODUCTION: test # (Optional) Add steps for generating coverage report and other post-test tasks publish: name: Publish Docker Image runs-on: ubuntu-latest needs: test + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: snackscription_review + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout Repository uses: actions/checkout@v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6dbb8fa..050ab36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,12 +3,7 @@ name: Java CI Pipeline on: push: branches: - - main - - dev - pull_request: - branches: - - main - - dev + - "**" workflow_dispatch: jobs: @@ -70,8 +65,10 @@ jobs: distribution: "temurin" java-version: "21" cache: "gradle" + - name: Make gradlew executable run: chmod +x ./gradlew + - name: Cache Gradle dependencies uses: actions/cache@v4 with: @@ -85,4 +82,6 @@ jobs: ./gradlew check --info --stacktrace ./gradlew test ./gradlew jacocoTestReport - # (Optional) Add steps for generating coverage report and other post-test tasks + env: + PRODUCTION: test + # (Optional) Add steps for generating coverage report and other post-test tasks \ No newline at end of file diff --git a/.monitoring/docker-compose.yml b/.monitoring/docker-compose.yml new file mode 100644 index 0000000..87b2c31 --- /dev/null +++ b/.monitoring/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3.7' + +services: + prometheus: + image: prom/prometheus:v2.44.0 + container_name: prometheus + ports: + - "9090:9090" + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + + grafana: + image: grafana/grafana:9.5.2 + container_name: grafana + ports: + - "3000:3000" + restart: unless-stopped + volumes: + - ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources diff --git a/.monitoring/grafana/provisioning/datasources/datasources.yml b/.monitoring/grafana/provisioning/datasources/datasources.yml new file mode 100644 index 0000000..8d9f9d8 --- /dev/null +++ b/.monitoring/grafana/provisioning/datasources/datasources.yml @@ -0,0 +1,7 @@ +apiVersion: 1 +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true \ No newline at end of file diff --git a/.monitoring/prometheus/prometheus.yml b/.monitoring/prometheus/prometheus.yml new file mode 100644 index 0000000..f454fff --- /dev/null +++ b/.monitoring/prometheus/prometheus.yml @@ -0,0 +1,11 @@ +scrape_configs: + - job_name: 'Snackscription Metrics' + metrics_path: '/actuator/prometheus' + scrape_interval: 3s + static_configs: + - targets: ['host.docker.internal:8080'] + labels: + application: 'Snackscription Review' + - targets: ['34.124.152.90'] + labels: + application: 'Snackscription Review (deployed)' \ No newline at end of file diff --git a/application.yml b/application.yml new file mode 100644 index 0000000..3feefab --- /dev/null +++ b/application.yml @@ -0,0 +1,5 @@ +management: + endpoints: + web: + exposure: + include: [ "prometheus" ] diff --git a/build.gradle.kts b/build.gradle.kts index c18a996..20bcb2e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("org.springframework.boot") version "3.2.5" id("io.spring.dependency-management") version "1.1.4" jacoco + id("org.sonarqube") version "4.4.1.3373" } group = "snackscription" @@ -12,6 +13,7 @@ java { sourceCompatibility = JavaVersion.VERSION_21 } + configurations { compileOnly { extendsFrom(configurations.annotationProcessor.get()) @@ -26,6 +28,9 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("io.micrometer:micrometer-registry-prometheus") + implementation("io.micrometer:micrometer-core") compileOnly("org.projectlombok:lombok") developmentOnly("org.springframework.boot:spring-boot-devtools") runtimeOnly("org.postgresql:postgresql") @@ -48,8 +53,17 @@ tasks.jacocoTestReport { })) dependsOn(tasks.test) reports { - xml.required.set(false) + xml.required.set(true) + html.required.set(true) csv.required.set(false) html.outputLocation.set(layout.buildDirectory.dir("jacocoHtml")) } +} + +sonar { + properties { + property("sonar.projectKey","ADPRO-C11_snackscription-review") + property("sonar.organization", "adpro-c11") + property("sonar.host.url", "https://sonarcloud.io") + } } \ No newline at end of file diff --git a/deployment.yaml b/deployment.yaml new file mode 100644 index 0000000..a4401a5 --- /dev/null +++ b/deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: snackscription-review-deployment +spec: + replicas: 3 + selector: + matchLabels: + app: snackscription-review + template: + metadata: + labels: + app: snackscription-review + spec: + containers: + - name: snackscription-review + image: asteriskzie/snackscription-review:latest + ports: + - containerPort: 8080 + env: + - name: PRODUCTION + value: prod \ No newline at end of file diff --git a/gradle/gradle.properties b/gradle/gradle.properties new file mode 100644 index 0000000..2b4c21a --- /dev/null +++ b/gradle/gradle.properties @@ -0,0 +1,5 @@ +systemProp.sonar.host.url=https://sonarcloud.io + +# Token generated from an account with 'Execute analysis' permission. +# It can also be set with the environment variable SONAR_TOKEN. +systemProp.sonar.token=${SONAR_TOKEN} \ No newline at end of file diff --git a/gradlew b/gradlew index 1aa94a4..e7c6edb 100755 --- a/gradlew +++ b/gradlew @@ -205,7 +205,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# * For example: A author cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ diff --git a/service.yaml b/service.yaml new file mode 100644 index 0000000..25b65be --- /dev/null +++ b/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: snackscription-review-service +spec: + type: LoadBalancer + ports: + - port: 80 + targetPort: 8080 + selector: + app: snackscription-review diff --git a/src/main/java/snackscription/review/ReviewAppConfig.java b/src/main/java/snackscription/review/ReviewAppConfig.java new file mode 100644 index 0000000..e3519e7 --- /dev/null +++ b/src/main/java/snackscription/review/ReviewAppConfig.java @@ -0,0 +1,14 @@ +package snackscription.review; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ReviewAppConfig { + @Bean + public MeterRegistry getMeterRegistry() { + return new CompositeMeterRegistry(); + } +} diff --git a/src/main/java/snackscription/review/ReviewApplication.java b/src/main/java/snackscription/review/ReviewApplication.java index 4898e1d..ec64e1c 100644 --- a/src/main/java/snackscription/review/ReviewApplication.java +++ b/src/main/java/snackscription/review/ReviewApplication.java @@ -2,28 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import java.util.concurrent.Executor; - -@EnableAsync @SpringBootApplication public class ReviewApplication { public static void main(String[] args) { SpringApplication.run(ReviewApplication.class, args); - } - @Bean - public Executor taskExecutor () { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(1); - executor.setMaxPoolSize(1); - executor.setQueueCapacity(500); - executor.setThreadNamePrefix("GithubLookup-"); - executor.initialize(); - return executor; + } + } diff --git a/src/main/java/snackscription/review/controller/ReviewAdminController.java b/src/main/java/snackscription/review/controller/ReviewAdminController.java new file mode 100644 index 0000000..25b7d0b --- /dev/null +++ b/src/main/java/snackscription/review/controller/ReviewAdminController.java @@ -0,0 +1,49 @@ +package snackscription.review.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import snackscription.review.model.Review; +import snackscription.review.service.ReviewServiceImpl; + +import java.util.List; + +@CrossOrigin +@RestController +@RequestMapping("/admin") +public class ReviewAdminController { + private ReviewServiceImpl reviewService; + + public ReviewAdminController(ReviewServiceImpl reviewService) { + this.reviewService = reviewService; + } + + @PutMapping("/subscription-boxes/{subsbox}/users/{user}/approve") + public ResponseEntity approveReview(@PathVariable String subsbox, @PathVariable String user) { + try { + Review review = reviewService.approveReview(subsbox, user); + return new ResponseEntity<>(review, HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } + + @PutMapping("/subscription-boxes/{subsbox}/users/{user}/reject") + public ResponseEntity rejectReview(@PathVariable String subsbox, @PathVariable String user) { + try { + Review review = reviewService.rejectReview(subsbox, user); + return new ResponseEntity<>(review, HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } + + @GetMapping("/subscription-boxes/{subsbox}/reviews") + public ResponseEntity> getSubsboxReviews( + @PathVariable String subsbox, + @RequestParam(required = false) String state) throws Exception { + List reviews = reviewService.getSubsboxReview(subsbox, state); + return ResponseEntity.ok().body(reviews); + } + +} diff --git a/src/main/java/snackscription/review/controller/ReviewController.java b/src/main/java/snackscription/review/controller/ReviewController.java index 4b89ab2..d04bdd2 100644 --- a/src/main/java/snackscription/review/controller/ReviewController.java +++ b/src/main/java/snackscription/review/controller/ReviewController.java @@ -1,39 +1,26 @@ package snackscription.review.controller; -import java.util.ArrayList; import java.util.List; import java.util.Map; -import org.apache.catalina.connector.Response; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.web.ServerProperties.Tomcat.Resource; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -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.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + import snackscription.review.model.Review; -import snackscription.review.repository.ReviewRepository; import snackscription.review.service.ReviewService; +import snackscription.review.service.ReviewServiceImpl; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.PutMapping; - - - - +@CrossOrigin @RestController -@RequestMapping("/") +@RequestMapping("") public class ReviewController { - - private ReviewService reviewService; + public static final String BODY_AUTHOR = "author"; + public static final String BODY_CONTENT = "content"; + public static final String BODY_RATING = "rating"; + public ReviewController(ReviewService reviewService) { this.reviewService = reviewService; } @@ -43,104 +30,63 @@ public ResponseEntity reviewPage() { return ResponseEntity.ok().body("Welcome to the review service!"); } - @PostMapping("/api/subscription-boxes/{subscriptionBoxId}") - public ResponseEntity createSubscriptionBoxReview(@RequestBody Map body, @PathVariable String subscriptionBoxId) { - + @PostMapping("/subscription-boxes/{subsbox}/users/self") + public ResponseEntity createSelfSubsboxReview(@RequestBody Map body, @PathVariable String subsbox) { try { - String userId = body.get("userId"); - int rating = Integer.parseInt(body.get("rating")); - String content = body.get("content"); + String author = body.get(BODY_AUTHOR); + int rating = Integer.parseInt(body.get(BODY_RATING)); + String content = body.get(BODY_CONTENT); - Review review = reviewService.createReview(rating, content, subscriptionBoxId, userId); + Review review = reviewService.createReview(rating, content, subsbox, author); return new ResponseEntity<>(review, HttpStatus.CREATED); } catch (Exception e) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } - @GetMapping("/api/subscription-boxes/{subscriptionBoxId}") - public ResponseEntity> getAllPublicSubscriptionBoxReview(@PathVariable String subscriptionBoxId) { + @GetMapping("/subscription-boxes/{subsbox}") + public ResponseEntity> getPublicSubsboxReview(@PathVariable String subsbox) { try { - List reviews = reviewService.getAllSubscriptionBoxReview(subscriptionBoxId, "APPROVED"); + List reviews = reviewService.getSubsboxReview(subsbox, "APPROVED"); return new ResponseEntity<>(reviews, HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } - @GetMapping("/api/subscription-boxes/{subscriptionBoxId}/users/self") - public ResponseEntity getSelfSubscriptionBoxReview(@RequestBody Map body, @PathVariable String subscriptionBoxId) { + @GetMapping("/subscription-boxes/{subsbox}/users/self") + public ResponseEntity getSelfSubsboxReview(@RequestBody Map body, @PathVariable String subsbox) { try { - String userId = body.get("userId"); - Review review = reviewService.getReview(subscriptionBoxId, userId); + String author = body.get("author"); // TODO: nanti pakai JWT token untuk ambil sendernya + Review review = reviewService.getReview(subsbox, author); return new ResponseEntity<>(review, HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } - @PutMapping("/api/subscription-boxes/{subscriptionBoxId}/users/self") - public ResponseEntity editSelfSubscriptionBoxId(@RequestBody Map body, @PathVariable String subscriptionBoxId) { + @PutMapping("/subscription-boxes/{subsbox}/users/self") + public ResponseEntity editSelfReview(@RequestBody Map body, @PathVariable String subsbox) { try { - String userId = body.get("userId"); + String author = body.get("author"); // TODO: nanti pakai JWT token untuk ambil sendernya int rating = Integer.parseInt(body.get("rating")); String content = body.get("content"); - Review review = reviewService.editReview(rating, content, subscriptionBoxId, userId); + Review review = reviewService.editReview(rating, content, subsbox, author); return new ResponseEntity<>(review, HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } - @DeleteMapping("/api/subscription-boxes/{subscriptionBoxId}/users/self") - public ResponseEntity deleteSelfSubscriptionBoxReview(@RequestBody Map body, @PathVariable String subscriptionBoxId) { + @DeleteMapping("/subscription-boxes/{subsbox}/users/self") + public ResponseEntity deleteSelfReview(@RequestBody Map body, @PathVariable String subsbox) { try { - String userId = body.get("userId"); - reviewService.deleteReview(subscriptionBoxId, userId); + String author = body.get("author"); // TODO: nanti pakai JWT token untuk ambil sendernya + reviewService.deleteReview(subsbox, author); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } catch (Exception e) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } - - @DeleteMapping("/api/subscription-boxes/{subscriptionBoxId}/users/{userId}") - public ResponseEntity deleteSubscriptionBoxReview(@PathVariable String subscriptionBoxId, @PathVariable String userId) { - try { - reviewService.deleteReview(subscriptionBoxId, userId); - return new ResponseEntity<>(HttpStatus.NO_CONTENT); - } catch (Exception e) { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); - } - } - - @GetMapping("/api/reviews/{subsboxId}") - public List getBySubscriptionBoxId(@PathVariable String subsboxId) throws Exception { - return reviewService.getAllSubscriptionBoxReview(subsboxId, null); - } - - @GetMapping("/api/reviews/{reviewId}") - public Review getById(@PathVariable String reviewId) throws Exception { - return reviewService.findById(reviewId); - } - - @PutMapping("/api/reviews/{reviewId}/approve") - public ResponseEntity approveReview(@PathVariable String reviewId) { - try { - Review review = reviewService.approveReview(reviewId); - return new ResponseEntity<>(review, HttpStatus.OK); - } catch (Exception e) { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); - } - } - - @PutMapping("/api/reviews/{reviewId}/reject") - public ResponseEntity rejectReview(@PathVariable String reviewId) { - try { - Review review = reviewService.rejectReview(reviewId); - return new ResponseEntity<>(review, HttpStatus.OK); - } catch (Exception e) { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); - } - } } \ No newline at end of file diff --git a/src/main/java/snackscription/review/model/Review.java b/src/main/java/snackscription/review/model/Review.java index 572d7a5..0ef1c69 100644 --- a/src/main/java/snackscription/review/model/Review.java +++ b/src/main/java/snackscription/review/model/Review.java @@ -1,19 +1,15 @@ package snackscription.review.model; -import lombok.Getter; -import lombok.Setter; - -import java.util.UUID; +import lombok.Data; import jakarta.persistence.*; -@Getter -@Setter +@Data @Entity -@Table(name = "review") +@Table public class Review { - @Id - private String id; + @EmbeddedId + private ReviewId id; @Column(name = "rating", nullable = false) private int rating; @@ -24,22 +20,14 @@ public class Review { @Column(name = "state", nullable = false) private ReviewState state; - @Column(name="user_id", nullable = false) - private String userId; - - @Column(name="subsbox_id", nullable = false) - private String subscriptionBoxId; - public Review() { } - public Review(int rating, String content, String userId, String subscriptionBoxId) { - this.id = UUID.randomUUID().toString(); + public Review(int rating, String content, String subsbox, String user) { + this.id = new ReviewId(subsbox, user); this.rating = rating; this.content = content; this.state = ReviewState.PENDING; - this.userId = userId; - this.subscriptionBoxId = subscriptionBoxId; } public void editReview(int rating, String content) { @@ -61,4 +49,12 @@ public void approve() { public void reject() { this.state.reject(this); } + + public String getSubsbox() { + return this.id.getSubsbox(); + } + + public String getAuthor() { + return this.id.getAuthor(); + } } \ No newline at end of file diff --git a/src/main/java/snackscription/review/model/ReviewId.java b/src/main/java/snackscription/review/model/ReviewId.java new file mode 100644 index 0000000..d1656fb --- /dev/null +++ b/src/main/java/snackscription/review/model/ReviewId.java @@ -0,0 +1,25 @@ +package snackscription.review.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Data; + +import java.io.Serializable; + +@Data +@Embeddable +public class ReviewId implements Serializable { + @Column(name = "subsbox", nullable = false) + private String subsbox; + + @Column(name = "author", nullable = false) + private String author; + + public ReviewId(String subsbox, String author) { + this.subsbox = subsbox; + this.author = author; + } + public ReviewId() { + + } +} diff --git a/src/main/java/snackscription/review/repository/ReviewRepository.java b/src/main/java/snackscription/review/repository/ReviewRepository.java index bb448bf..f0f025e 100644 --- a/src/main/java/snackscription/review/repository/ReviewRepository.java +++ b/src/main/java/snackscription/review/repository/ReviewRepository.java @@ -2,14 +2,24 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.CrudRepository; + +import io.micrometer.common.lang.Nullable; import snackscription.review.model.Review; +import snackscription.review.model.ReviewId; import snackscription.review.model.ReviewState; -public interface ReviewRepository extends JpaRepository { - List findBySubscriptionBoxId(String subsboxId); - List findBySubscriptionBoxIdAndState(String subsboxId, ReviewState state); - Review findBySubscriptionBoxIdAndUserId(String subsboxId, String userId); - void deleteBySubscriptionBoxIdAndUserId(String subsboxId, String userId); +public interface ReviewRepository extends JpaRepository { + @Nullable + List findByIdSubsbox(String subsbox); + + @Nullable + List findByIdAuthor(String author); + + @Nullable + List findByIdSubsboxAndState(String subsbox, ReviewState state); + + @Nullable + Review findByIdSubsboxAndIdAuthor(String subsbox, String author); + + void deleteByIdSubsboxAndIdAuthor(String subsbox, String author); } \ No newline at end of file diff --git a/src/main/java/snackscription/review/service/ReviewService.java b/src/main/java/snackscription/review/service/ReviewService.java index 266d3ba..aee0263 100644 --- a/src/main/java/snackscription/review/service/ReviewService.java +++ b/src/main/java/snackscription/review/service/ReviewService.java @@ -1,102 +1,15 @@ package snackscription.review.service; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -import snackscription.review.exception.InvalidStateException; -import snackscription.review.exception.ReviewNotFoundException; import snackscription.review.model.Review; -import snackscription.review.model.ReviewState; -import snackscription.review.repository.ReviewRepository; - -@Service -public class ReviewService { - private ReviewRepository reviewRepository; - private SentimentAnalysisService sentimentAnalysisService; - - public ReviewService (ReviewRepository reviewRepository) { - this.reviewRepository = reviewRepository; - } - - public Review findById(String reviewId) throws ReviewNotFoundException { - Optional oReview = reviewRepository.findById(reviewId); - - if (oReview.isEmpty()) { - throw new ReviewNotFoundException(); - } - - return oReview.get(); - } - - public List findBySubscriptionBoxId(String subscriptionBoxId) { - return reviewRepository.findBySubscriptionBoxId(subscriptionBoxId); - } - - public Review createReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception { - Review review = new Review(rating, content, userId, subscriptionBoxId); - reviewRepository.save(review); - return review; - } - - public List getAllSubscriptionBoxReview(String subscriptionBoxId, String state) throws Exception { - if (state == null) { - return reviewRepository.findBySubscriptionBoxId(subscriptionBoxId); - } else { - state = state.toUpperCase(); - ReviewState reviewState = Enum.valueOf(ReviewState.class, state); - if (reviewState == null) { - throw new InvalidStateException(); - } - return reviewRepository.findBySubscriptionBoxIdAndState(subscriptionBoxId, reviewState); - } - } - - public Review getReview(String subscriptionBoxId, String userId) throws Exception { - return reviewRepository.findBySubscriptionBoxIdAndUserId(subscriptionBoxId, userId); - } - - public Review editReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception { - Review review = reviewRepository.findBySubscriptionBoxIdAndUserId(subscriptionBoxId, userId); - - if (review == null) { - throw new ReviewNotFoundException(); - } - - review.setRating(rating); - review.setContent(content); - - return reviewRepository.save(review); - } - - public void deleteReview(String subscriptionBoxId, String userId) throws Exception { - Review review = reviewRepository.findBySubscriptionBoxIdAndUserId(subscriptionBoxId, userId); - - if (review == null) { - throw new ReviewNotFoundException(); - } - - reviewRepository.delete(review); - } - - public Review approveReview(String reviewId) throws Exception { - Review review = findById(reviewId); - review.approve(); - return reviewRepository.save(review); - } - - public Review rejectReview(String reviewId) throws Exception { - Review review = findById(reviewId); - review.reject(); - return reviewRepository.save(review); - } +import java.util.List; - @Async - public CompletableFuture analyzeSentimentAsync(String reviewText) { - String sentiment = sentimentAnalysisService.analyze(reviewText); - return CompletableFuture.completedFuture(sentiment); - } +public interface ReviewService { + public boolean reviewExist(String subsbox, String user); + public Review createReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception; + public Review getReview(String subsbox, String user) throws Exception; + public List getSubsboxReview(String subscriptionBoxId, String state) throws Exception; + public Review editReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception; + public void deleteReview(String subsbox, String user) throws Exception; + public Review approveReview(String subsbox, String user) throws Exception; + public Review rejectReview(String subsbox, String user) throws Exception; } diff --git a/src/main/java/snackscription/review/service/ReviewServiceImpl.java b/src/main/java/snackscription/review/service/ReviewServiceImpl.java new file mode 100644 index 0000000..34dd81c --- /dev/null +++ b/src/main/java/snackscription/review/service/ReviewServiceImpl.java @@ -0,0 +1,117 @@ +package snackscription.review.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; + +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +import snackscription.review.exception.InvalidStateException; +import snackscription.review.exception.ReviewNotFoundException; +import snackscription.review.model.Review; +import snackscription.review.model.ReviewId; +import snackscription.review.model.ReviewState; +import snackscription.review.repository.ReviewRepository; + +@Service +@Component +public class ReviewServiceImpl implements ReviewService { + private final ReviewRepository reviewRepository; + private final SentimentAnalysisService sentimentAnalysisService; + + public ReviewServiceImpl (ReviewRepository reviewRepository, SentimentAnalysisService sentimentAnalysisService) { + this.reviewRepository = reviewRepository; + this.sentimentAnalysisService = sentimentAnalysisService; + } + + public boolean reviewExist(String subsbox, String user) { + return reviewRepository.existsById(new ReviewId(subsbox, user)); + } + + public Review createReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception { + if (reviewExist(subscriptionBoxId, userId)) { + throw new Exception("User has made a review for this subscription box."); + } + + if (rating < 1 || rating > 5) { + throw new Exception("Rating out of range"); + } + + Review review = new Review(rating, content, subscriptionBoxId, userId); + reviewRepository.save(review); + return review; + } + + public Review getReview(String subsbox, String user) throws Exception { + Optional oreview = reviewRepository.findById(new ReviewId(subsbox, user)); + if (oreview.isEmpty()) { + throw new ReviewNotFoundException(); + } + return oreview.get(); + } + + public List getSubsboxReview(String subscriptionBoxId, String state) throws Exception { + List result; + if (state == null) { + result = reviewRepository.findByIdSubsbox(subscriptionBoxId); + } else { + state = state.toUpperCase(); + if (!state.equals("PENDING") && !state.equals("APPROVED") && !state.equals("REJECTED")) { + throw new InvalidStateException(); + } + ReviewState reviewState = ReviewState.valueOf(state); + result = reviewRepository.findByIdSubsboxAndState(subscriptionBoxId, reviewState); + } + + if (result == null) { + result = new ArrayList<>(); + } + return result; + } + + public Review editReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception { + Review review = reviewRepository.findByIdSubsboxAndIdAuthor(subscriptionBoxId, userId); + + if (review == null) { + throw new ReviewNotFoundException(); + } + + review.setRating(rating); + review.setContent(content); + + return reviewRepository.save(review); + } + + public Review approveReview(String subsbox, String user) throws Exception { + Review review = getReview(subsbox, user); + review.approve(); + return reviewRepository.save(review); + } + + public Review rejectReview(String subsbox, String user) throws Exception { + Review review = getReview(subsbox, user); + review.reject(); + return reviewRepository.save(review); + } + + public void deleteReview(String subsbox, String user) throws Exception { + Review review = reviewRepository.findByIdSubsboxAndIdAuthor(subsbox, user); + + if (review == null) { + throw new ReviewNotFoundException(); + } + + reviewRepository.delete(review); + } + + @Async + public CompletableFuture analyzeSentimentAsync(String reviewText) { + String sentiment = sentimentAnalysisService.analyze(reviewText); + return CompletableFuture.completedFuture(sentiment); + } +} diff --git a/src/main/java/snackscription/review/service/SentimentAnalysisService.java b/src/main/java/snackscription/review/service/SentimentAnalysisService.java index 1ee68cf..d09357b 100644 --- a/src/main/java/snackscription/review/service/SentimentAnalysisService.java +++ b/src/main/java/snackscription/review/service/SentimentAnalysisService.java @@ -1,5 +1,10 @@ package snackscription.review.service; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +@Service +@Component public class SentimentAnalysisService { public String analyze(String reviewText) { // do analysis diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index dfdbb9f..083d035 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -1,4 +1,4 @@ -spring.datasource.url=jdbc:postgresql://localhost:5432/snackscription_review +spring.datasource.url=jdbc:postgresql://localhost:5433/snackscription_review spring.datasource.username=postgres spring.datasource.password=postgres spring.jpa.hibernate.ddl-auto=create-drop diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 338aaa3..1bd6102 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,4 @@ spring.application.name=review -spring.profiles.active=${PRODUCTION:dev} \ No newline at end of file +spring.profiles.active=${PRODUCTION:dev} +management.endpoint.info.enabled=true +management.endpoints.web.exposure.include=health,metrics,prometheus,loggers \ No newline at end of file diff --git a/src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java b/src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java new file mode 100644 index 0000000..75cf3f4 --- /dev/null +++ b/src/test/java/snackscription/review/controller/ReviewAdminControllerTest.java @@ -0,0 +1,149 @@ +package snackscription.review.controller; + +import com.jayway.jsonpath.JsonPath; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import org.springframework.test.web.servlet.ResultActions; +import snackscription.review.model.Review; +import snackscription.review.model.ReviewState; +import snackscription.review.service.ReviewServiceImpl; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +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.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +@WebMvcTest(ReviewAdminController.class) +public class ReviewAdminControllerTest { + @MockBean + ReviewServiceImpl reviewService; + + @Autowired + MockMvc mockMvc; + + List reviews; + static final String BASE_URL = "/admin"; + + @BeforeEach + public void setUp() { + this.reviews = new ArrayList<>(); + + Review review1 = new Review(5, "I love it", "subsbox_123", "user_123"); + Review review2 = new Review(1, "I hate it", "subsbox_123", "user_124"); + Review review3 = new Review(2, "Hmmmm idk", "subsbox_124", "user_124"); + Review review4 = new Review(3, "It's okay", "subsbox_124", "user_125"); + Review review5 = new Review(4, "I like it", "subsbox_124", "user_126"); + + review4.setState(ReviewState.APPROVED); + review5.setState(ReviewState.REJECTED); + + reviews = new ArrayList<>(); + reviews.add(review1); + reviews.add(review2); + reviews.add(review3); + reviews.add(review4); + reviews.add(review5); + } + + @Test + public void testApproveReview() throws Exception { + Review review = reviews.getFirst(); + + Review approvedReview = new Review(review.getRating(), review.getContent(), review.getSubsbox(), review.getAuthor()); + approvedReview.setState(ReviewState.APPROVED); + + when(reviewService.approveReview(review.getSubsbox(), review.getAuthor())).thenReturn(approvedReview); + + ResultActions result = mockMvc.perform(put(BASE_URL + "/subscription-boxes/{subsboxId}/users/{userId}/approve", review.getSubsbox(), review.getAuthor())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.rating", is(review.getRating()))) + .andExpect(jsonPath("$.content", is(review.getContent()))) + .andExpect(jsonPath("$.author", is(review.getAuthor()))) + .andExpect(jsonPath("$.subsbox", is(review.getSubsbox()))) + .andExpect(jsonPath("$.state", is("APPROVED"))); + + verify(reviewService).approveReview(review.getSubsbox(), review.getAuthor()); + } + + @Test + public void testRejectReview() throws Exception { + Review review = reviews.getFirst(); + + Review rejectedReview = new Review(review.getRating(), review.getContent(), review.getSubsbox(), review.getAuthor()); + rejectedReview.setState(ReviewState.REJECTED); + + when(reviewService.rejectReview(review.getSubsbox(), review.getAuthor())).thenReturn(rejectedReview); + + ResultActions result = mockMvc.perform(put(BASE_URL + "/subscription-boxes/{subsboxId}/users/{userId}/reject", review.getSubsbox(), review.getAuthor())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.rating", is(review.getRating()))) + .andExpect(jsonPath("$.content", is(review.getContent()))) + .andExpect(jsonPath("$.author", is(review.getAuthor()))) + .andExpect(jsonPath("$.subsbox", is(review.getSubsbox()))) + .andExpect(jsonPath("$.state", is("REJECTED"))); + + verify(reviewService).rejectReview(review.getSubsbox(), review.getAuthor()); + } + + @Test + public void testGetAllPendingReview() throws Exception { + List pendingReviews = new ArrayList<>(); + String subsboxId = "subsbox_124"; + + for (Review review : reviews) { + if (review.getSubsbox().equals(subsboxId) && review.getState().equals(ReviewState.PENDING)) pendingReviews.add(review); + } + + when(reviewService.getSubsboxReview(subsboxId, ReviewState.PENDING.toString())).thenReturn(pendingReviews); + + String result = mockMvc.perform( + get(BASE_URL + "/subscription-boxes/{subsboxId}/reviews?state=PENDING", subsboxId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(pendingReviews.size()))) + .andReturn().getResponse().getContentAsString(); + + List foundReviews = new ArrayList(); + for (int i=0; i cmp = Comparator.comparing(Review::getAuthor); + pendingReviews.sort(cmp); + foundReviews.sort(cmp); + + for (int i=0; i(); - Review review1 = new Review(5, "I love it", "user_123", "subsbox_123"); - Review review2 = new Review(1, "I hate it", "user_124", "subsbox_123"); - Review review3 = new Review(2, "Hmmmm idk", "user_124", "subsbox_124"); - Review review4 = new Review(3, "It's okay", "user_125", "subsbox_124"); - Review review5 = new Review(4, "I like it", "user_126", "subsbox_124"); + Review review1 = new Review(5, "I love it", "subsbox_123", "user_123"); + Review review2 = new Review(1, "I hate it", "subsbox_123", "user_124"); + Review review3 = new Review(2, "Hmmmm idk", "subsbox_124", "user_124"); + Review review4 = new Review(3, "It's okay", "subsbox_124", "user_125"); + Review review5 = new Review(4, "I like it", "subsbox_124", "user_126"); review1.setState(ReviewState.PENDING); review4.setState(ReviewState.APPROVED); @@ -73,36 +64,36 @@ public void setUp() { } @Test - public void testCreateSubscriptionBoxReview() throws Exception{ + public void testCreateSubsboxReview() throws Exception{ Review review = reviews.getFirst(); - when(reviewService.createReview(review.getRating(), review.getContent(), review.getSubscriptionBoxId(), review.getUserId())).thenReturn(review); + when(reviewService.createReview(review.getRating(), review.getContent(), review.getId().getSubsbox(), review.getId().getAuthor())).thenReturn(review); - ResultActions result = mockMvc.perform(post("/api/subscription-boxes/{subscriptionBoxId}", review.getSubscriptionBoxId()) + ResultActions result = mockMvc.perform(post("/subscription-boxes/{subsbox}/users/self", review.getSubsbox()) .contentType(MediaType.APPLICATION_JSON) - .content("{\"rating\": 5, \"content\": \"I love it\", \"userId\": \"user_123\"}")) + .content("{\"rating\": 5, \"content\": \"I love it\", \"author\": \"user_123\"}")) .andExpect(status().isCreated()) .andExpect(jsonPath("$.rating", is(5))) .andExpect(jsonPath("$.content", is("I love it"))) - .andExpect(jsonPath("$.userId", is("user_123"))) - .andExpect(jsonPath("$.subscriptionBoxId", is("subsbox_123"))); + .andExpect(jsonPath("$.author", is("user_123"))) + .andExpect(jsonPath("$.subsbox", is("subsbox_123"))); - verify(reviewService).createReview(review.getRating(), review.getContent(), review.getSubscriptionBoxId(), review.getUserId()); + verify(reviewService).createReview(review.getRating(), review.getContent(), review.getSubsbox(), review.getAuthor()); } @Test - public void testReadAllPublicSubscriptionBoxReview() throws Exception { + public void testReadAllPublicSubsboxReviews() throws Exception { List approvedReviews = new ArrayList<>(); - String subsboxId = "subsbox_124"; + String subsbox = "subsbox_124"; for (Review review : reviews) { - if (review.getSubscriptionBoxId().equals(subsboxId) && review.getState().equals(ReviewState.APPROVED)) { + if (review.getSubsbox().equals(subsbox) && review.getState().equals(ReviewState.APPROVED)) { approvedReviews.add(review); } } - when(reviewService.getAllSubscriptionBoxReview(subsboxId, "APPROVED")).thenReturn(approvedReviews); + when(reviewService.getSubsboxReview(subsbox, "APPROVED")).thenReturn(approvedReviews); - String result = mockMvc.perform(get("/api/subscription-boxes/{subscriptionBoxId}", subsboxId)) + String result = mockMvc.perform(get("/subscription-boxes/{subsbox}", subsbox)) .andExpect(status().isOk()) .andExpect(jsonPath("$", hasSize(approvedReviews.size()))) .andReturn() @@ -114,241 +105,82 @@ public void testReadAllPublicSubscriptionBoxReview() throws Exception { String prefixMatcher = String.format("$[%d]", i); int rating = JsonPath.read(result, prefixMatcher + ".rating"); String content = JsonPath.read(result, prefixMatcher + ".content"); - String userId = JsonPath.read(result, prefixMatcher + ".userId"); - String curSubscriptionBoxId = JsonPath.read(result, prefixMatcher + ".subscriptionBoxId"); + String author = JsonPath.read(result, prefixMatcher + ".author"); + String curSubscriptionBoxId = JsonPath.read(result, prefixMatcher + ".subsbox"); - Review review = new Review(rating, content, userId, curSubscriptionBoxId); + Review review = new Review(rating, content, curSubscriptionBoxId, author); foundReviews.add(review); } - Comparator cmp = new Comparator() { - @Override - public int compare(Review o1, Review o2) { - return o1.getUserId().compareTo(o2.getUserId()); - } - }; - + Comparator cmp = Comparator.comparing(Review::getAuthor); approvedReviews.sort(cmp); foundReviews.sort(cmp); for (int i=0; i reviews = new ArrayList<>(); -// reviews.add(new Review(5, "amazing", "user1", subsboxId)); -// reviews.add(new Review(4, "good", "user2", subsboxId)); - -// when(reviewService.testGetAllSubscriptionBoxReview(subsboxId)).thenReturn(reviews); - -// ResultActions result = mockMvc.perform(get("/api/subscription-boxes/{subsboxId}", subsboxId)) -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$", hasSize(2))) -// .andExpect(jsonPath("$[0].rating", is(5))) -// .andExpect(jsonPath("$[0].content", is("amazing"))) -// .andExpect(jsonPath("$[0].userId", is("user1"))) -// .andExpect(jsonPath("$[0].subscriptionBoxId", is(subsboxId))) -// .andExpect(jsonPath("$[1].rating", is(4))) -// .andExpect(jsonPath("$[1].content", is("good"))) -// .andExpect(jsonPath("$[1].userId", is("user2"))) -// .andExpect(jsonPath("$[1].subscriptionBoxId", is(subsboxId))); - -// verify(reviewService).testGetAllSubscriptionBoxReview(subsboxId); -// } - -// @Test -// public void testGetById() throws Exception { -// Review review = new Review( -// 5, "amazing", "user1", "subsboxId" -// ); -// String reviewId = review.getId(); - -// when(reviewService.findById(reviewId)).thenReturn(review); - -// ResultActions result = mockMvc.perform(get("/api/reviews/{reviewId}", reviewId)) -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$.rating", is(5))) -// .andExpect(jsonPath("$.content", is("amazing"))) -// .andExpect(jsonPath("$.userId", is("user1"))) -// .andExpect(jsonPath("$.subscriptionBoxId", is("subsboxId"))); - -// verify(reviewService).findById(reviewId); -// } - -// @Test -// public void testGetBySubscriptionBoxId() throws Exception { -// List curReviews = new ArrayList<>(); - -// String subscriptionBoxId = this.reviews.getFirst().getSubscriptionBoxId(); -// for (Review review : this.reviews) { -// if (review.getSubscriptionBoxId().equals(subscriptionBoxId)) { -// curReviews.add(review); -// } -// } - -// when(reviewService.findBySubscriptionBoxId(subscriptionBoxId)).thenReturn(curReviews); - -// String result = mockMvc.perform(get("/api/subscription-boxes/{subscriptionBoxId}", subscriptionBoxId)) -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$", hasSize(curReviews.size()))) -// .andReturn() -// .getResponse() -// .getContentAsString(); - -// List foundReviews = new ArrayList(); -// for (int i=0; i cmp = new Comparator() { -// @Override -// public int compare(Review o1, Review o2) { -// return o1.getUserId().compareTo(o2.getUserId()); -// } -// }; - -// curReviews.sort(cmp); -// foundReviews.sort(cmp); - -// for (int i=0; i(); - - Review review1 = new Review(5, "I love it", "user_123", "subsbox_123"); - Review review2 = new Review(1, "I hate it", "user_124", "subsbox_123"); - Review review3 = new Review(2, "Hmmmm idk", "user_124", "subsbox_124"); - Review review4 = new Review(3, "It's okay", "user_125", "subsbox_124"); - Review review5 = new Review(4, "I like it", "user_126", "subsbox_124"); + + Review review1 = new Review(5, "I love it", "subsbox_123", "user_123"); + Review review2 = new Review(1, "I hate it", "subsbox_123", "user_124"); + Review review3 = new Review(2, "Hmmmm idk", "subsbox_124", "user_124"); + Review review4 = new Review(3, "It's okay", "subsbox_124", "user_125"); + Review review5 = new Review(4, "I like it", "subsbox_124", "user_126"); review1.setState(ReviewState.PENDING); review4.setState(ReviewState.APPROVED); @@ -50,14 +49,14 @@ public void setUp() { public void testFindBySubscriptionBoxId() { List curReviews = new ArrayList<>(); - String subsbox_id = this.reviews.getFirst().getSubscriptionBoxId(); + String subsbox_id = this.reviews.getFirst().getSubsbox(); for (Review review : this.reviews) { - if (review.getSubscriptionBoxId().equals(subsbox_id)) { + if (review.getSubsbox().equals(subsbox_id)) { curReviews.add(review); } } - List foundReviews = reviewRepository.findBySubscriptionBoxId(subsbox_id); + List foundReviews = reviewRepository.findByIdSubsbox(subsbox_id); assertEquals(curReviews.size(), foundReviews.size()); for (int i=0; i curReviews = new ArrayList<>(); - String subsbox_id = this.reviews.getFirst().getSubscriptionBoxId(); + String subsbox_id = this.reviews.getFirst().getSubsbox(); for (Review review : this.reviews) { - if (review.getSubscriptionBoxId().equals(subsbox_id) && review.getState().equals(ReviewState.APPROVED)){ + if (review.getSubsbox().equals(subsbox_id) && review.getState().equals(ReviewState.APPROVED)){ curReviews.add(review); } } - List foundReviews = reviewRepository.findBySubscriptionBoxIdAndState(subsbox_id, ReviewState.APPROVED); + List foundReviews = reviewRepository.findByIdSubsboxAndState(subsbox_id, ReviewState.APPROVED); assertEquals(curReviews.size(), foundReviews.size()); for (int i=0; i reviews; - // @Test - // public void testGetAllSubscriptionBoxReview() { - // ReviewService reviewService = new ReviewService(reviewRepo); - - // Optional review = Optional.of(new Review( - // 5, "amazing", "user1", "subsboxId" - // )); - - // when(reviewRepo.findById("subsboxId")).thenReturn(review); - - // Review foundReview = reviewService.getAllSubscriptionBoxReview("subsboxId"); - - // assertEquals(review.get(), foundReview); - - // verify(reviewRepo).findBySubscriptionBoxId("subsboxId"); - - // } - @BeforeEach public void setUp() { - reviewService = new ReviewService(reviewRepo); sentimentAnalysisService = new SentimentAnalysisService(); + reviewService = new ReviewServiceImpl(reviewRepo, sentimentAnalysisService); - Review review1 = new Review(5, "I love it", "user_123", "subsbox_123"); - Review review2 = new Review(1, "I hate it", "user_124", "subsbox_123"); - Review review3 = new Review(2, "Hmmmm idk", "user_124", "subsbox_124"); - Review review4 = new Review(3, "It's okay", "user_125", "subsbox_124"); - Review review5 = new Review(4, "I like it", "user_126", "subsbox_124"); + Review review1 = new Review(5, "I love it", "subsbox_123", "user_123"); + Review review2 = new Review(1, "I hate it", "subsbox_123", "user_124"); + Review review3 = new Review(2, "Hmmmm idk", "subsbox_124", "user_124"); + Review review4 = new Review(3, "It's okay", "subsbox_124", "user_125"); + Review review5 = new Review(4, "I like it", "subsbox_124", "user_126"); review1.setState(ReviewState.PENDING); review4.setState(ReviewState.APPROVED); @@ -82,75 +57,81 @@ public void setUp() { reviews.add(review5); } - @Test - public void getReviewById() throws Exception { - - Optional review = Optional.of(new Review( - 5, "amazing", "user1", "subsboxId" - )); - - String reviewId = review.get().getId(); + @Test + public void testGetSubsboxReview() throws Exception { + String subscriptionBoxId = this.reviews.getFirst().getSubsbox(); + List curReviews = new ArrayList<>(); + for (Review review : this.reviews) { + if (review.getSubsbox().equals(subscriptionBoxId)) { + curReviews.add(review); + } + } - when(reviewRepo.findById(reviewId)).thenReturn(review); + when(reviewRepo.findByIdSubsbox(subscriptionBoxId)).thenReturn(curReviews); - Review foundReview = reviewService.findById(reviewId); + List foundReviews = reviewService.getSubsboxReview(subscriptionBoxId, null); - assertEquals(foundReview, review.get()); + assertEquals(curReviews, foundReviews); - verify(reviewRepo).findById(reviewId); + verify(reviewRepo).findByIdSubsbox(subscriptionBoxId); } @Test - public void getReviewByIdNotFound() { - ReviewService reviewService = new ReviewService(reviewRepo); + public void testGetSubsboxReviewNotFound() throws Exception { + String subscriptionBoxId = "nonexistent_subsbox_id"; - Optional review = Optional.empty(); + when(reviewRepo.findByIdSubsbox(subscriptionBoxId)).thenReturn(null); - String reviewId = "reviewId"; + List foundReviews = reviewService.getSubsboxReview(subscriptionBoxId, null); - when(reviewRepo.findById(reviewId)).thenReturn(review); - - assertThrows(ReviewNotFoundException.class, () -> { - reviewService.findById(reviewId); - }); + assertNotNull(foundReviews); + assertEquals(0, foundReviews.size()); - verify(reviewRepo).findById(reviewId); + verify(reviewRepo).findByIdSubsbox(subscriptionBoxId); } @Test - public void getReviewsBySubscriptionBoxId() { - ReviewService reviewService = new ReviewService(reviewRepo); + public void testGetSubsboxReviewApproved() throws Exception { + String subscriptionBoxId = this.reviews.getFirst().getSubsbox(); - List curReviews = new ArrayList<>(); + List cuReviews = new ArrayList<>(); - String subscriptionBoxId = this.reviews.getFirst().getSubscriptionBoxId(); for (Review review : this.reviews) { - if (review.getSubscriptionBoxId().equals(subscriptionBoxId)) { - curReviews.add(review); + if (review.getSubsbox().equals(subscriptionBoxId) && review.getState().equals(ReviewState.APPROVED)) { + cuReviews.add(review); } } - when(reviewRepo.findBySubscriptionBoxId(subscriptionBoxId)).thenReturn(curReviews); + when(reviewRepo.findByIdSubsboxAndState(subscriptionBoxId, ReviewState.APPROVED)).thenReturn(cuReviews); - List foundReviews = reviewService.findBySubscriptionBoxId(subscriptionBoxId); + List foundReviews = reviewService.getSubsboxReview(subscriptionBoxId, "APPROVED"); - assertEquals(curReviews, foundReviews); + assertEquals(cuReviews, foundReviews); + + verify(reviewRepo).findByIdSubsboxAndState(subscriptionBoxId, ReviewState.APPROVED); + } + + @Test + public void testGetSubsboxReviewInvalidState() throws Exception { + String subscriptionBoxId = this.reviews.getFirst().getSubsbox(); - verify(reviewRepo).findBySubscriptionBoxId(subscriptionBoxId); + assertThrows(InvalidStateException.class, () -> { + reviewService.getSubsboxReview(subscriptionBoxId, "INVALID_STATE"); + }); } @Test public void testCreateReview() throws Exception { Review review = reviews.getFirst(); - + when(reviewRepo.save(any(Review.class))).thenReturn(review); Review savedReview = reviewService.createReview( review.getRating(), review.getContent(), - review.getSubscriptionBoxId(), - review.getUserId()); + review.getSubsbox(), + review.getAuthor()); assertEqualReview(review, savedReview); @@ -158,108 +139,99 @@ public void testCreateReview() throws Exception { } @Test - public void testGetAllSubscriptionBoxReview() throws Exception { - String subscriptionBoxId = this.reviews.getFirst().getSubscriptionBoxId(); - - List curReviews = new ArrayList<>(); - - for (Review review : this.reviews) { - if (review.getSubscriptionBoxId().equals(subscriptionBoxId)) { - curReviews.add(review); - } - } + public void testCreateReviewAlreadyExist() throws Exception { + Review review = reviews.get(0); + when(reviewRepo.existsById(review.getId())).thenReturn(true); - when(reviewRepo.findBySubscriptionBoxId(subscriptionBoxId)).thenReturn(curReviews); - - List foundReviews = reviewService.getAllSubscriptionBoxReview(subscriptionBoxId, null); - - assertEquals(curReviews, foundReviews); + assertThrows(Exception.class, () -> { + reviewService.createReview(review.getRating(), review.getContent(), review.getSubsbox(), review.getAuthor()); + }); - verify(reviewRepo).findBySubscriptionBoxId(subscriptionBoxId); + verify(reviewRepo).existsById(review.getId()); } @Test - public void testGetAllSubscriptionBoxReviewApproved() throws Exception { - String subscriptionBoxId = this.reviews.getFirst().getSubscriptionBoxId(); - - List cuReviews = new ArrayList<>(); - - for (Review review : this.reviews) { - if (review.getSubscriptionBoxId().equals(subscriptionBoxId) && review.getState().equals(ReviewState.APPROVED)) { - cuReviews.add(review); - } - } - - when(reviewRepo.findBySubscriptionBoxIdAndState(subscriptionBoxId, ReviewState.APPROVED)).thenReturn(cuReviews); - - List foundReviews = reviewService.getAllSubscriptionBoxReview(subscriptionBoxId, "APPROVED"); - - assertEquals(cuReviews, foundReviews); - - verify(reviewRepo).findBySubscriptionBoxIdAndState(subscriptionBoxId, ReviewState.APPROVED); + public void testCreateReviewInvalidRating() throws Exception { + Review review = reviews.get(0); + assertThrows(Exception.class, () -> { + reviewService.createReview(-1, review.getContent(), review.getSubsbox(), review.getAuthor()); + }); } @Test public void testEditReview() throws Exception { Review review = reviews.getFirst(); - String subscriptionBoxId = review.getSubscriptionBoxId(); - String userId = review.getUserId(); + String subsbox = review.getSubsbox(); + String author = review.getAuthor(); int newRating = 1; String newContent = "Changed content"; - Review newReview = new Review(newRating, newContent, userId, subscriptionBoxId); + Review newReview = new Review(newRating, newContent, author, subsbox); newReview.setId(review.getId()); - when(reviewRepo.findBySubscriptionBoxIdAndUserId(subscriptionBoxId, userId)).thenReturn(review); + when(reviewRepo.findByIdSubsboxAndIdAuthor(subsbox, author)).thenReturn(review); when(reviewRepo.save(any(Review.class))).thenReturn(newReview); - Review editedReview = reviewService.editReview(newRating, newContent, subscriptionBoxId, userId); + Review editedReview = reviewService.editReview(newRating, newContent, subsbox, author); assertEquals(newRating, editedReview.getRating()); assertEquals(newContent, editedReview.getContent()); - assertEquals(subscriptionBoxId, editedReview.getSubscriptionBoxId()); - assertEquals(userId, editedReview.getUserId()); + assertEquals(subsbox, editedReview.getSubsbox()); + assertEquals(author, editedReview.getAuthor()); assertEquals(review.getId(), editedReview.getId()); } + @Test + public void testEditReviewNotFound() throws Exception { + Review review = reviews.getFirst(); + String subsbox = review.getSubsbox(); + String author = review.getAuthor(); + + int newRating = 1; + String newContent = "Changed content"; + Review newReview = new Review(newRating, newContent, author, subsbox); + newReview.setId(review.getId()); + + when(reviewRepo.findByIdSubsboxAndIdAuthor(subsbox, author)).thenReturn(null); + + assertThrows(ReviewNotFoundException.class, () -> { + reviewService.editReview(newRating, newContent, subsbox, author); + }); + } + @Test public void testDeleteReview() throws Exception { - String subscriptionBoxId = this.reviews.getFirst().getSubscriptionBoxId(); - String userId = this.reviews.getFirst().getUserId(); + String subsbox = this.reviews.getFirst().getSubsbox(); + String author = this.reviews.getFirst().getAuthor(); Review review = reviews.getFirst(); - when(reviewRepo.findBySubscriptionBoxIdAndUserId(subscriptionBoxId, userId)).thenReturn(review); + when(reviewRepo.findByIdSubsboxAndIdAuthor(subsbox, author)).thenReturn(review); - reviewService.deleteReview(subscriptionBoxId, userId); + reviewService.deleteReview(subsbox, author); - when(reviewRepo.findBySubscriptionBoxIdAndUserId(subscriptionBoxId, userId)).thenReturn(null); - - assertNull(reviewService.getReview(subscriptionBoxId, userId)); + assertThrows(ReviewNotFoundException.class, () -> reviewService.getReview(subsbox, author)); verify(reviewRepo).delete(review); } - public void assertEqualReview(Review review1, Review review2) { - assertEquals(review1.getRating(), review2.getRating()); - assertEquals(review1.getContent(), review2.getContent()); - assertEquals(review1.getUserId(), review2.getUserId()); - assertEquals(review1.getSubscriptionBoxId(), review2.getSubscriptionBoxId()); - } - @Test - public void testAnalyzeSentimentAsyncTest() { - String reviewText = "This is a great product!"; - String expectedSentiment = "positive"; + public void testDeleteReviewNotFound() throws Exception { + String subsbox = this.reviews.getFirst().getSubsbox(); + String author = this.reviews.getFirst().getAuthor(); - when(sentimentAnalysisService.analyze(reviewText)).thenReturn(expectedSentiment); + Review review = reviews.getFirst(); - CompletableFuture sentimentFuture = reviewService.analyzeSentimentAsync(reviewText); + when(reviewRepo.findByIdSubsboxAndIdAuthor(subsbox, author)).thenReturn(null); - String actualSentiment = sentimentFuture.join(); - verify(sentimentAnalysisService).analyze(reviewText); + assertThrows(ReviewNotFoundException.class, () -> reviewService.deleteReview(subsbox, author)); + } - assertEquals(expectedSentiment, actualSentiment); + public void assertEqualReview(Review review1, Review review2) { + assertEquals(review1.getRating(), review2.getRating()); + assertEquals(review1.getContent(), review2.getContent()); + assertEquals(review1.getAuthor(), review2.getAuthor()); + assertEquals(review1.getSubsbox(), review2.getSubsbox()); } }