diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 30e9471..40fcb7c 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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c793b8b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,89 @@ +name: Java CI Pipeline + +on: + push: + branches: + - "**" + workflow_dispatch: + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "21" + cache: "gradle" + + - name: Cache Gradle dependencies + uses: actions/cache@v4 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Make gradlew executable + run: chmod +x ./gradlew + - name: Build with Gradle + run: | + ./gradlew assemble + # (Optional) Add steps for running tests and generating reports + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: java-app + path: build/libs/*.jar + + test: + 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 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "21" + cache: "gradle" + + + - name: Make gradlew executable + run: chmod +x ./gradlew + + + - name: Cache Gradle dependencies + uses: actions/cache@v4 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Test with Gradle + run: | + ./gradlew check --info --stacktrace + ./gradlew test + ./gradlew jacocoTestReport + 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/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 f1016f9..a05fb4c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,10 +28,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") 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 4342da7..d04bdd2 100644 --- a/src/main/java/snackscription/review/controller/ReviewController.java +++ b/src/main/java/snackscription/review/controller/ReviewController.java @@ -9,9 +9,11 @@ import snackscription.review.model.Review; import snackscription.review.service.ReviewService; +import snackscription.review.service.ReviewServiceImpl; +@CrossOrigin @RestController -@RequestMapping("/reviews") +@RequestMapping("") public class ReviewController { private ReviewService reviewService; @@ -28,8 +30,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(BODY_AUTHOR); int rating = Integer.parseInt(body.get(BODY_RATING)); @@ -42,7 +44,7 @@ public ResponseEntity createSubsboxReview(@RequestBody Map> getPublicSubsboxReview(@PathVariable String subsbox) { try { List reviews = reviewService.getSubsboxReview(subsbox, "APPROVED"); @@ -52,74 +54,39 @@ 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(BODY_AUTHOR); - 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(BODY_AUTHOR); - if (!authenticate(sender, user)) { - return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); - } - - int rating = Integer.parseInt(body.get(BODY_RATING)); - String content = body.get(BODY_CONTENT); + 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); } } - - @GetMapping("/subscription-boxes/{subsbox}") - 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/main/java/snackscription/review/repository/ReviewRepository.java b/src/main/java/snackscription/review/repository/ReviewRepository.java index 096b317..19b3c56 100644 --- a/src/main/java/snackscription/review/repository/ReviewRepository.java +++ b/src/main/java/snackscription/review/repository/ReviewRepository.java @@ -2,14 +2,25 @@ 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.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/main/java/snackscription/review/service/ReviewService.java b/src/main/java/snackscription/review/service/ReviewService.java index 31a94c5..aee0263 100644 --- a/src/main/java/snackscription/review/service/ReviewService.java +++ b/src/main/java/snackscription/review/service/ReviewService.java @@ -1,88 +1,15 @@ package snackscription.review.service; -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 ReviewRepository reviewRepository; - - public ReviewService (ReviewRepository reviewRepository) { - this.reviewRepository = reviewRepository; - } - - public Review createReview(int rating, String content, String subscriptionBoxId, String userId) throws Exception { - 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(user, subsbox)); - if (oreview.isEmpty()) { - throw new ReviewNotFoundException(); - } - return oreview.get(); - } - - public List getSubsboxReview(String subscriptionBoxId, String state) throws Exception { - if (state == null) { - return reviewRepository.findByIdSubsbox(subscriptionBoxId); - } else { - state = state.toUpperCase(); - ReviewState reviewState = Enum.valueOf(ReviewState.class, state); - if (reviewState == null) { - throw new InvalidStateException(); - } - return reviewRepository.findByIdSubsboxAndState(subscriptionBoxId, reviewState); - } - } - - 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..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 new file mode 100644 index 0000000..d09357b --- /dev/null +++ b/src/main/java/snackscription/review/service/SentimentAnalysisService.java @@ -0,0 +1,14 @@ +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 + return "positive"; + } + +} 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 approvedReviews = new ArrayList<>(); String subsbox = "subsbox_124"; - for (Review review : reviews) { + for (Review review : reviews) { if (review.getSubsbox().equals(subsbox) && review.getState().equals(ReviewState.APPROVED)) { approvedReviews.add(review); } } - when(reviewService.getSubsboxReview(subsbox, "APPROVED")).thenReturn(approvedReviews); + 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}", subsbox)) .andExpect(status().isOk()) .andExpect(jsonPath("$", hasSize(approvedReviews.size()))) .andReturn() .getResponse() .getContentAsString(); - List foundReviews = new ArrayList(); - for (int i=0; i foundReviews = new ArrayList(); + for (int i=0; i cmp = Comparator.comparing(Review::getAuthor); approvedReviews.sort(cmp); @@ -120,21 +123,21 @@ public void testReadAllPublicSubscriptionBoxReview() throws Exception { assertEquals(approvedReviews.get(i).getRating(), foundReviews.get(i).getRating()); assertEquals(approvedReviews.get(i).getContent(), foundReviews.get(i).getContent()); assertEquals(approvedReviews.get(i).getAuthor(), foundReviews.get(i).getAuthor()); - assertEquals(approvedReviews.get(i).getSubsbox(), foundReviews.get(i).getSubsbox()); + assertEquals(approvedReviews.get(i).getSubsbox(), foundReviews.get(i).getSubsbox()); } - verify(reviewService).getSubsboxReview(subsbox, "APPROVED"); + verify(reviewService).getSubsboxReview(subsbox, "APPROVED"); } @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); + 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/self", subsbox, author) .contentType(MediaType.APPLICATION_JSON) .content("{\"author\": \"user_123\"}")) .andExpect(status().isOk()) @@ -146,58 +149,42 @@ public void readSelfSubscriptionBoxReview() throws Exception { verify(reviewService).getReview(subsbox, author); } - @Test - public void testDeleteUserSubscriptionBoxReview() 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("/reviews/subscription-boxes/{subsbox}/users/{author}", subsbox, author) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNoContent()); - - verify(reviewService).deleteReview(subsbox, author); - } - @Test - public void testApproveReview() throws Exception { + public void testEditSelfSubsboxReview() throws Exception { Review review = reviews.getFirst(); + String subsboxId = review.getSubsbox(); + String userId = review.getAuthor(); - Review approvedReview = new Review(review.getRating(), review.getContent(), review.getSubsbox(), review.getAuthor()); - approvedReview.setState(ReviewState.APPROVED); + int newRating = 4; + String newContent = "Awikwok"; + when(reviewService.editReview(newRating, newContent, subsboxId, userId)).thenReturn(new Review(newRating, newContent, subsboxId, userId)); - 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())) + 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(review.getRating()))) - .andExpect(jsonPath("$.content", is(review.getContent()))) + .andExpect(jsonPath("$.rating", is(newRating))) + .andExpect(jsonPath("$.content", is(newContent))) .andExpect(jsonPath("$.author", is(review.getAuthor()))) - .andExpect(jsonPath("$.subsbox", is(review.getSubsbox()))) - .andExpect(jsonPath("$.state", is("APPROVED"))); + .andExpect(jsonPath("$.subsbox", is(review.getSubsbox()))); - verify(reviewService).approveReview(review.getSubsbox(), review.getAuthor()); + verify(reviewService).editReview(newRating, newContent, subsboxId, userId); } @Test - public void testRejectReview() throws Exception { + public void testDeleteSelfSubsboxReview() throws Exception { Review review = reviews.getFirst(); + String subsbox = review.getSubsbox(); + String author = review.getAuthor(); - 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); + doNothing().when(reviewService).deleteReview(subsbox, author); + doNothing().when(reviewService).deleteReview(subsbox, author); - 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"))); + ResultActions result = mockMvc.perform(delete("/subscription-boxes/{subsbox}/users/self", subsbox) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"author\": \"user_123\"}")) + .andExpect(status().isNoContent()); - verify(reviewService).approveReview(review.getSubsbox(), review.getAuthor()); + verify(reviewService).deleteReview(subsbox, author); } } diff --git a/src/test/java/snackscription/review/service/ReviewServiceTest.java b/src/test/java/snackscription/review/service/ReviewServiceTest.java index 6ac24a3..edd0b7a 100644 --- a/src/test/java/snackscription/review/service/ReviewServiceTest.java +++ b/src/test/java/snackscription/review/service/ReviewServiceTest.java @@ -1,7 +1,8 @@ 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.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @@ -16,6 +17,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; @@ -23,7 +25,9 @@ @ExtendWith(MockitoExtension.class) public class ReviewServiceTest { - + + @Mock + SentimentAnalysisService sentimentAnalysisService; @Mock ReviewRepository reviewRepo; @@ -33,7 +37,8 @@ public class ReviewServiceTest { @BeforeEach public void setUp() { - reviewService = new ReviewService(reviewRepo); + sentimentAnalysisService = new SentimentAnalysisService(); + reviewService = new ReviewServiceImpl(reviewRepo, sentimentAnalysisService); Review review1 = new Review(5, "I love it", "subsbox_123", "user_123"); Review review2 = new Review(1, "I hate it", "subsbox_123", "user_124"); @@ -53,12 +58,12 @@ 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(); - for (Review review : this.reviews) { + List curReviews = new ArrayList<>(); + for (Review review : this.reviews) { if (review.getSubsbox().equals(subscriptionBoxId)) { curReviews.add(review); } @@ -66,53 +71,29 @@ public void getReviewsBySubscriptionBoxId() throws Exception { when(reviewRepo.findByIdSubsbox(subscriptionBoxId)).thenReturn(curReviews); - List foundReviews = reviewService.getSubsboxReview(subscriptionBoxId, null); + List foundReviews = reviewService.getSubsboxReview(subscriptionBoxId, null); assertEquals(curReviews, foundReviews); verify(reviewRepo).findByIdSubsbox(subscriptionBoxId); } - @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.getSubsbox(), - review.getAuthor()); - - assertEqualReview(review, savedReview); - - verify(reviewRepo).save(any(Review.class)); - } - - @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); - } - } + @Test + public void testGetSubsboxReviewNotFound() throws Exception { + String subscriptionBoxId = "nonexistent_subsbox_id"; - when(reviewRepo.findByIdSubsbox(subscriptionBoxId)).thenReturn(curReviews); + when(reviewRepo.findByIdSubsbox(subscriptionBoxId)).thenReturn(null); List foundReviews = reviewService.getSubsboxReview(subscriptionBoxId, null); - assertEquals(curReviews, foundReviews); + assertNotNull(foundReviews); + assertEquals(0, foundReviews.size()); verify(reviewRepo).findByIdSubsbox(subscriptionBoxId); } @Test - public void testgetSubsboxReviewApproved() throws Exception { + public void testGetSubsboxReviewApproved() throws Exception { String subscriptionBoxId = this.reviews.getFirst().getSubsbox(); List cuReviews = new ArrayList<>(); @@ -132,6 +113,52 @@ 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(); + + when(reviewRepo.save(any(Review.class))).thenReturn(review); + + Review savedReview = reviewService.createReview( + review.getRating(), + review.getContent(), + review.getSubsbox(), + review.getAuthor()); + + assertEqualReview(review, savedReview); + + verify(reviewRepo).save(any(Review.class)); + } + + @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 testEditReview() throws Exception { Review review = reviews.getFirst(); @@ -140,25 +167,46 @@ public void testEditReview() throws Exception { int newRating = 1; String newContent = "Changed content"; - Review newReview = new Review(newRating, newContent, author, subsbox); + 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(review); when(reviewRepo.save(any(Review.class))).thenReturn(newReview); - Review editedReview = reviewService.editReview(newRating, newContent, subsbox, author); + 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(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 subsbox = this.reviews.getFirst().getSubsbox(); - String author = this.reviews.getFirst().getAuthor(); + String author = this.reviews.getFirst().getAuthor(); Review review = reviews.getFirst(); @@ -171,6 +219,18 @@ 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(null); + + assertThrows(ReviewNotFoundException.class, () -> reviewService.deleteReview(subsbox, author)); + } + public void assertEqualReview(Review review1, Review review2) { assertEquals(review1.getRating(), review2.getRating()); assertEquals(review1.getContent(), review2.getContent());