From bf05c58b65b94b857033a1087da4cc6bc2b310ec Mon Sep 17 00:00:00 2001 From: Khanh Nguyen <119989010+ndkhanh-axonivy@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:38:34 +0700 Subject: [PATCH 1/3] Feature/MARP 357 create detail pages for new market website rating (#28) --- .github/workflows/service-dev-build.yml | 2 +- marketplace-service/pom.xml | 10 + .../market/MarketplaceServiceApplication.java | 6 +- .../assembler/FeedbackModelAssembler.java | 57 ++++++ .../assembler/ProductModelAssembler.java | 11 +- .../config/MarketApiDocumentConfig.java | 10 +- .../config/MarketHeaderInterceptor.java | 8 +- .../axonivy/market/config/MongoConfig.java | 11 +- .../market/constants/EntityConstants.java | 1 + .../market/constants/GitHubConstants.java | 17 ++ .../constants/RequestMappingConstants.java | 1 + .../market/controller/AppController.java | 12 +- .../market/controller/FeedbackController.java | 107 ++++++++++ .../market/controller/OAuth2Controller.java | 48 +++++ .../market/controller/ProductController.java | 44 ++-- .../market/controller/UserController.java | 27 --- .../com/axonivy/market/entity/Feedback.java | 55 +++++ .../axonivy/market/entity/GitHubRepoMeta.java | 7 +- .../market/entity/MavenArtifactModel.java | 9 +- .../java/com/axonivy/market/entity/User.java | 47 ++++- .../com/axonivy/market/enums/ErrorCode.java | 6 +- .../com/axonivy/market/enums/FileStatus.java | 4 +- .../com/axonivy/market/enums/FileType.java | 4 +- .../com/axonivy/market/enums/SortOption.java | 4 +- .../com/axonivy/market/enums/TypeOption.java | 4 +- .../market/exceptions/ExceptionHandlers.java | 44 +++- .../model/InvalidParamException.java | 1 - .../model/MissingHeaderException.java | 3 + .../exceptions/model/NotFoundException.java | 4 +- .../model/Oauth2ExchangeCodeException.java | 19 ++ .../market/github/model/ArchivedArtifact.java | 1 - .../market/github/model/GitHubFile.java | 5 +- .../com/axonivy/market/github/model/Meta.java | 5 +- .../service/GHAxonIvyMarketRepoService.java | 7 +- .../market/github/service/GitHubService.java | 22 +- .../impl/GHAxonIvyMarketRepoServiceImpl.java | 27 +-- .../service/impl/GitHubServiceImpl.java | 89 ++++++++- .../axonivy/market/model/DisplayValue.java | 6 +- .../axonivy/market/model/FeedbackModel.java | 42 ++++ .../market/model/MultilingualismValue.java | 4 +- .../market/model/Oauth2AuthorizationCode.java | 12 ++ .../axonivy/market/model/ProductModel.java | 14 +- .../axonivy/market/model/ProductRating.java | 16 ++ .../market/repository/FeedbackRepository.java | 21 ++ .../repository/GitHubRepoMetaRepository.java | 3 +- .../market/repository/ProductRepository.java | 3 +- .../market/repository/UserRepository.java | 1 + .../market/schedulingtask/ScheduledTasks.java | 6 +- .../market/service/FeedbackService.java | 17 ++ .../axonivy/market/service/JwtService.java | 10 + .../market/service/ProductService.java | 3 +- .../axonivy/market/service/UserService.java | 7 +- .../service/impl/FeedbackServiceImpl.java | 102 ++++++++++ .../market/service/impl/JwtServiceImpl.java | 54 +++++ .../market/service/impl/UserServiceImpl.java | 18 +- .../src/main/resources/application.properties | 4 + .../market/controller/AppControllerTest.java | 6 +- .../controller/FeedbackControllerTest.java | 161 +++++++++++++++ .../controller/OAuth2ControllerTest.java | 66 ++++++ .../controller/ProductControllerTest.java | 39 ++-- .../market/controller/UserControllerTest.java | 27 --- .../market/handler/ExceptionHandlersTest.java | 17 +- .../service/FeedbackServiceImplTest.java | 189 ++++++++++++++++++ .../GHAxonIvyMarketRepoServiceImplTest.java | 31 ++- .../market/service/GitHubServiceImplTest.java | 35 +++- .../market/service/JwtServiceImplTest.java | 94 +++++++++ .../market/service/SchedulingTasksTest.java | 7 +- .../market/service/UserServiceImplTest.java | 27 ++- 68 files changed, 1490 insertions(+), 291 deletions(-) create mode 100644 marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java delete mode 100644 marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/ProductRating.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/repository/FeedbackRepository.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java delete mode 100644 marketplace-service/src/test/java/com/axonivy/market/controller/UserControllerTest.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java diff --git a/.github/workflows/service-dev-build.yml b/.github/workflows/service-dev-build.yml index e310a9378..3700e5322 100644 --- a/.github/workflows/service-dev-build.yml +++ b/.github/workflows/service-dev-build.yml @@ -39,4 +39,4 @@ jobs: - name: Restart Tomcat server run: | sudo systemctl stop tomcat - sudo systemctl start tomcat + sudo systemctl start tomcat \ No newline at end of file diff --git a/marketplace-service/pom.xml b/marketplace-service/pom.xml index 947e700d7..d6a0d9a67 100644 --- a/marketplace-service/pom.xml +++ b/marketplace-service/pom.xml @@ -64,6 +64,16 @@ github-api 1.321 + + io.jsonwebtoken + jjwt + 0.9.1 + + + javax.xml.bind + jaxb-api + 2.3.1 + diff --git a/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java b/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java index 06660037c..52cdb27d9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java +++ b/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java @@ -1,5 +1,7 @@ package com.axonivy.market; +import com.axonivy.market.service.ProductService; +import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.time.StopWatch; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -9,10 +11,6 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; -import com.axonivy.market.service.ProductService; - -import lombok.extern.log4j.Log4j2; - @Log4j2 @EnableAsync @EnableScheduling diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java new file mode 100644 index 000000000..a981b099a --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java @@ -0,0 +1,57 @@ +package com.axonivy.market.assembler; + +import com.axonivy.market.controller.FeedbackController; +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.FeedbackModel; +import com.axonivy.market.service.UserService; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; +import org.springframework.stereotype.Component; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +@Log4j2 +@Component +public class FeedbackModelAssembler extends RepresentationModelAssemblerSupport { + + private final UserService userService; + + public FeedbackModelAssembler(UserService userService) { + super(Feedback.class, FeedbackModel.class); + this.userService = userService; + } + + @Override + public FeedbackModel toModel(Feedback feedback) { + FeedbackModel resource = new FeedbackModel(); + resource.add(linkTo(methodOn(FeedbackController.class).findFeedback(feedback.getId())) + .withSelfRel()); + return createResource(resource, feedback); + } + + private FeedbackModel createResource(FeedbackModel model, Feedback feedback) { + User user; + try { + user = userService.findUser(feedback.getUserId()); + } + catch (NotFoundException e) { + log.warn(e.getMessage()); + user = new User(); + } + model.setId(feedback.getId()); + model.setUsername(StringUtils.isBlank(user.getName()) ? user.getUsername() : user.getName()); + model.setUserAvatarUrl(user.getAvatarUrl()); + model.setUserProvider(user.getProvider()); + model.setProductId(feedback.getProductId()); + model.setContent(feedback.getContent()); + model.setRating(feedback.getRating()); + model.setCreatedAt(feedback.getCreatedAt()); + model.setUpdatedAt(feedback.getUpdatedAt()); + return model; + } + +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java index 00b94a52f..a50d82d1a 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java @@ -1,14 +1,13 @@ package com.axonivy.market.assembler; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; - -import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; -import org.springframework.stereotype.Component; - import com.axonivy.market.controller.ProductDetailsController; import com.axonivy.market.entity.Product; import com.axonivy.market.model.ProductModel; +import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; +import org.springframework.stereotype.Component; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; @Component public class ProductModelAssembler extends RepresentationModelAssemblerSupport { diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java index 61b6a0773..805f30120 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java @@ -1,15 +1,15 @@ package com.axonivy.market.config; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.parameters.Parameter; import org.springdoc.core.customizers.OpenApiCustomizer; import org.springdoc.core.models.GroupedOpenApi; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import io.swagger.v3.oas.models.Operation; -import io.swagger.v3.oas.models.PathItem; -import io.swagger.v3.oas.models.media.StringSchema; -import io.swagger.v3.oas.models.parameters.Parameter; -import static com.axonivy.market.constants.CommonConstants.*; +import static com.axonivy.market.constants.CommonConstants.REQUESTED_BY; @Configuration public class MarketApiDocumentConfig { diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java b/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java index 963706069..83b281062 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java @@ -1,15 +1,13 @@ package com.axonivy.market.config; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.exceptions.model.MissingHeaderException; - import io.swagger.v3.oas.models.PathItem.HttpMethod; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; @Component public class MarketHeaderInterceptor implements HandlerInterceptor { diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java index a6cd2bc05..7f558f1cc 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java @@ -1,10 +1,15 @@ package com.axonivy.market.config; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.config.EnableMongoAuditing; import org.springframework.data.mongodb.core.convert.DbRefResolver; import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; @@ -12,13 +17,9 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; -import com.mongodb.ConnectionString; -import com.mongodb.MongoClientSettings; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoClients; - @Configuration @EnableMongoRepositories(basePackages = "com.axonivy.market.repository") +@EnableMongoAuditing public class MongoConfig extends AbstractMongoClientConfiguration { @Value("${spring.data.mongodb.host}") diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java index 76c1c45ab..0d9752cb9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java @@ -9,4 +9,5 @@ public class EntityConstants { public static final String PRODUCT = "Product"; public static final String MAVEN_ARTIFACT_VERSION = "MavenArtifactVersion"; public static final String GH_REPO_META = "GitHubRepoMeta"; + public static final String FEEDBACK = "Feedback"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java index 33c84df88..39d35a77c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java @@ -10,4 +10,21 @@ public class GitHubConstants { public static final String AXONIVY_MARKETPLACE_PATH = "market"; public static final String DEFAULT_BRANCH = "feature/MARP-463-Multilingualism-for-Website"; public static final String PRODUCT_JSON_FILE_PATH_FORMAT = "%s/product.json"; + public static final String GITHUB_PROVIDER_NAME = "GitHub"; + public static final String GITHUB_GET_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"; + + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class Json { + public static final String ACCESS_TOKEN = "access_token"; + public static final String TOKEN = "token"; + public static final String CLIENT_ID = "client_id"; + public static final String CLIENT_SECRET = "client_secret"; + public static final String CODE = "code"; + public static final String ERROR = "error"; + public static final String ERROR_DESCRIPTION = "error"; + public static final String USER_ID = "id"; + public static final String USER_NAME = "name"; + public static final String USER_AVATAR_URL = "avatar_url"; + public static final String USER_LOGIN_NAME = "login"; + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java index f4687a442..5efdc47e6 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java @@ -11,5 +11,6 @@ public class RequestMappingConstants { public static final String USER_MAPPING = "/user"; public static final String PRODUCT = API + "/product"; public static final String PRODUCT_DETAILS = API + "/product-details"; + public static final String FEEDBACK = API + "/feedback"; public static final String SWAGGER_URL = "/swagger-ui/index.html"; } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java index 60523b72a..45f712d2d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java @@ -1,8 +1,8 @@ package com.axonivy.market.controller; -import static com.axonivy.market.constants.RequestMappingConstants.ROOT; -import static com.axonivy.market.constants.RequestMappingConstants.SWAGGER_URL; - +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.model.Message; +import lombok.extern.log4j.Log4j2; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -10,10 +10,8 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import com.axonivy.market.enums.ErrorCode; -import com.axonivy.market.model.Message; - -import lombok.extern.log4j.Log4j2; +import static com.axonivy.market.constants.RequestMappingConstants.ROOT; +import static com.axonivy.market.constants.RequestMappingConstants.SWAGGER_URL; @Log4j2 @RestController diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java new file mode 100644 index 000000000..3232f42be --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java @@ -0,0 +1,107 @@ +package com.axonivy.market.controller; + +import com.axonivy.market.assembler.FeedbackModelAssembler; +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.model.FeedbackModel; +import com.axonivy.market.model.ProductRating; +import com.axonivy.market.service.FeedbackService; +import com.axonivy.market.service.JwtService; +import io.jsonwebtoken.Claims; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.hateoas.PagedModel; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; +import java.util.List; + +import static com.axonivy.market.constants.RequestMappingConstants.FEEDBACK; + +@RestController +@RequestMapping(FEEDBACK) +public class FeedbackController { + + private final FeedbackService feedbackService; + private final JwtService jwtService; + private final FeedbackModelAssembler feedbackModelAssembler; + + private final PagedResourcesAssembler pagedResourcesAssembler; + + public FeedbackController(FeedbackService feedbackService, JwtService jwtService, FeedbackModelAssembler feedbackModelAssembler, PagedResourcesAssembler pagedResourcesAssembler) { + this.feedbackService = feedbackService; + this.jwtService = jwtService; + this.feedbackModelAssembler = feedbackModelAssembler; + this.pagedResourcesAssembler = pagedResourcesAssembler; + } + + @Operation(summary = "Find all feedbacks by product id") + @GetMapping("/product/{productId}") + public ResponseEntity> findFeedbacks(@PathVariable String productId, Pageable pageable) { + Page results = feedbackService.findFeedbacks(productId, pageable); + if (results.isEmpty()) { + return generateEmptyPagedModel(); + } + var responseContent = new PageImpl<>(results.getContent(), pageable, results.getTotalElements()); + var pageResources = pagedResourcesAssembler.toModel(responseContent, feedbackModelAssembler); + return new ResponseEntity<>(pageResources, HttpStatus.OK); + } + + @GetMapping("/{id}") + public ResponseEntity findFeedback(@PathVariable("id") String id) { + Feedback feedback = feedbackService.findFeedback(id); + return ResponseEntity.ok(feedbackModelAssembler.toModel(feedback)); + } + + @Operation(summary = "Find all feedbacks by user id and product id") + @GetMapping() + public ResponseEntity findFeedbackByUserIdAndProductId( + @RequestParam String userId, + @RequestParam String productId) { + Feedback feedback = feedbackService.findFeedbackByUserIdAndProductId(userId, productId); + return ResponseEntity.ok(feedbackModelAssembler.toModel(feedback)); + } + + @PostMapping + public ResponseEntity createFeedback(@RequestBody @Valid Feedback feedback, @RequestHeader(value = "Authorization", required = false) String authorizationHeader) { + String token = null; + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + token = authorizationHeader.substring(7); // Remove "Bearer " prefix + } + + // Validate the token + if (token == null || !jwtService.validateToken(token)) { + return ResponseEntity.status(401).build(); // Unauthorized if token is missing or invalid + } + + Claims claims = jwtService.getClaimsFromToken(token); + feedback.setUserId(claims.getSubject()); + Feedback newFeedback = feedbackService.upsertFeedback(feedback); + + URI location = ServletUriComponentsBuilder.fromCurrentRequest() + .path("/{id}") + .buildAndExpand(newFeedback.getId()) + .toUri(); + + return ResponseEntity.created(location).build(); + } + + @Operation(summary = "Find rating information of product by id") + @GetMapping("/product/{productId}/rating") + public ResponseEntity> getProductRating(@PathVariable("productId") String productId) { + return ResponseEntity.ok(feedbackService.getProductRatingById(productId)); + } + + @SuppressWarnings("unchecked") + private ResponseEntity> generateEmptyPagedModel() { + var emptyPagedModel = (PagedModel) pagedResourcesAssembler + .toEmptyModel(Page.empty(), FeedbackModel.class); + return new ResponseEntity<>(emptyPagedModel, HttpStatus.OK); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java new file mode 100644 index 000000000..a3ebbca64 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java @@ -0,0 +1,48 @@ +package com.axonivy.market.controller; + +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.User; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.Oauth2AuthorizationCode; +import com.axonivy.market.service.JwtService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.Map; + +@RestController +@RequestMapping("/auth") +public class OAuth2Controller { + + @Value("${spring.security.oauth2.client.registration.github.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.client.registration.github.client-secret}") + private String clientSecret; + + private final GitHubService gitHubService; + + private final JwtService jwtService; + + public OAuth2Controller(GitHubService gitHubService, JwtService jwtService) { + this.gitHubService = gitHubService; + this.jwtService = jwtService; + } + + @PostMapping("/github/login") + public ResponseEntity gitHubLogin(@RequestBody Oauth2AuthorizationCode oauth2AuthorizationCode) { + Map tokenResponse = gitHubService.getAccessToken(oauth2AuthorizationCode.getCode(), clientId, clientSecret); + String accessToken = (String) tokenResponse.get(GitHubConstants.Json.ACCESS_TOKEN); + + User user = gitHubService.getAndUpdateUser(accessToken); + + String jwtToken = jwtService.generateToken(user); + + return ResponseEntity.ok().body(Collections.singletonMap(GitHubConstants.Json.TOKEN, jwtToken)); + } +} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java index ce273cd33..6dbd73c4d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java @@ -1,8 +1,12 @@ package com.axonivy.market.controller; -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; -import static com.axonivy.market.constants.RequestMappingConstants.SYNC; - +import com.axonivy.market.assembler.ProductModelAssembler; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.model.Message; +import com.axonivy.market.model.ProductModel; +import com.axonivy.market.service.ProductService; +import io.swagger.v3.oas.annotations.Operation; import org.apache.commons.lang3.time.StopWatch; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -11,32 +15,22 @@ import org.springframework.hateoas.PagedModel; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import com.axonivy.market.assembler.ProductModelAssembler; -import com.axonivy.market.entity.Product; -import com.axonivy.market.enums.ErrorCode; -import com.axonivy.market.model.Message; -import com.axonivy.market.model.ProductModel; -import com.axonivy.market.service.ProductService; - -import io.swagger.v3.oas.annotations.Operation; +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; +import static com.axonivy.market.constants.RequestMappingConstants.SYNC; @RestController @RequestMapping(PRODUCT) public class ProductController { - private final ProductService service; + private final ProductService productService; private final ProductModelAssembler assembler; private final PagedResourcesAssembler pagedResourcesAssembler; - public ProductController(ProductService service, ProductModelAssembler assembler, - PagedResourcesAssembler pagedResourcesAssembler) { - this.service = service; + public ProductController(ProductService productService, ProductModelAssembler assembler, + PagedResourcesAssembler pagedResourcesAssembler) { + this.productService = productService; this.assembler = assembler; this.pagedResourcesAssembler = pagedResourcesAssembler; } @@ -44,14 +38,14 @@ public ProductController(ProductService service, ProductModelAssembler assembler @Operation(summary = "Find all products", description = "Be default system will finds product by type as 'all'") @GetMapping() public ResponseEntity> findProducts( - @RequestParam(required = true, name = "type") String type, + @RequestParam(name = "type") String type, @RequestParam(required = false, name = "keyword") String keyword, - @RequestParam(required = true, name = "language") String language, Pageable pageable) { - Page results = service.findProducts(type, keyword, language, pageable); + @RequestParam(name = "language") String language, Pageable pageable) { + Page results = productService.findProducts(type, keyword, language, pageable); if (results.isEmpty()) { return generateEmptyPagedModel(); } - var responseContent = new PageImpl(results.getContent(), pageable, results.getTotalElements()); + var responseContent = new PageImpl<>(results.getContent(), pageable, results.getTotalElements()); var pageResources = pagedResourcesAssembler.toModel(responseContent, assembler); return new ResponseEntity<>(pageResources, HttpStatus.OK); } @@ -60,7 +54,7 @@ public ResponseEntity> findProducts( public ResponseEntity syncProducts() { var stopWatch = new StopWatch(); stopWatch.start(); - var isAlreadyUpToDate = service.syncLatestDataFromMarketRepo(); + var isAlreadyUpToDate = productService.syncLatestDataFromMarketRepo(); var message = new Message(); message.setHelpCode(ErrorCode.SUCCESSFUL.getCode()); message.setHelpText(ErrorCode.SUCCESSFUL.getHelpText()); diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java deleted file mode 100644 index c83c7cc2b..000000000 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.axonivy.market.controller; - -import com.axonivy.market.entity.User; -import com.axonivy.market.service.UserService; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -import static com.axonivy.market.constants.RequestMappingConstants.USER_MAPPING; - -@RestController -@RequestMapping(USER_MAPPING) -public class UserController { - private final UserService userService; - - public UserController(UserService userService) { - this.userService = userService; - } - - @GetMapping - public ResponseEntity> getAllUser() { - return ResponseEntity.ok(userService.getAllUsers()); - } -} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java new file mode 100644 index 000000000..166da0c23 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java @@ -0,0 +1,55 @@ +package com.axonivy.market.entity; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +import static com.axonivy.market.constants.EntityConstants.FEEDBACK; + +@Getter +@Setter +@NoArgsConstructor +@Document(FEEDBACK) +public class Feedback implements Serializable { + + @Serial + private static final long serialVersionUID = 29519800556564714L; + + @Id + private String id; + + private String userId; + + @NotBlank(message = "Product id cannot be blank") + private String productId; + + @NotBlank(message = "Content cannot be blank") + @Size(max = 5, message = "Content length must be up to 250 characters") + private String content; + + @Min(value = 1, message = "Rating should not be less than 1") + @Max(value = 5, message = "Rating should not be greater than 5") + private Integer rating; + + @CreatedDate + private Date createdAt; + + @LastModifiedDate + private Date updatedAt; + + public void setContent(String content) { + this.content = content != null ? content.trim() : null; + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java b/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java index 2e0770816..d2ef46fbf 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java @@ -1,12 +1,11 @@ package com.axonivy.market.entity; -import static com.axonivy.market.constants.EntityConstants.GH_REPO_META; - +import lombok.Getter; +import lombok.Setter; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; -import lombok.Getter; -import lombok.Setter; +import static com.axonivy.market.constants.EntityConstants.GH_REPO_META; @Getter @Setter diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java index 6b5328e26..2d48d4c6a 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java @@ -1,14 +1,13 @@ package com.axonivy.market.entity; -import java.io.Serializable; -import java.util.Objects; - -import org.springframework.data.annotation.Transient; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.springframework.data.annotation.Transient; + +import java.io.Serializable; +import java.util.Objects; @AllArgsConstructor @NoArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/User.java b/marketplace-service/src/main/java/com/axonivy/market/entity/User.java index 1b88f1095..0f8e7b612 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/User.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/User.java @@ -1,21 +1,48 @@ package com.axonivy.market.entity; -import static com.axonivy.market.constants.EntityConstants.USER; - -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.io.Serial; +import java.io.Serializable; + +import static com.axonivy.market.constants.EntityConstants.USER; @Getter @Setter @NoArgsConstructor @Document(USER) -public class User { - @Id - private String id; - private String username; - private String password; +public class User implements Serializable { + @Serial + private static final long serialVersionUID = -1244486023332931059L; + + @Id + private String id; + + @Indexed(unique = true) + private String gitHubId; + + private String provider; + private String username; + private String name; + private String avatarUrl; + + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return new EqualsBuilder().append(id, ((User) obj).getId()).isEquals(); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java index de9d54c28..7aef2b47c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java @@ -18,8 +18,12 @@ public enum ErrorCode { SUCCESSFUL("0000", "SUCCESSFUL"), PRODUCT_FILTER_INVALID("1101", "PRODUCT_FILTER_INVALID"), PRODUCT_SORT_INVALID("1102", "PRODUCT_SORT_INVALID"), + PRODUCT_NOT_FOUND("1103", "PRODUCT_NOT_FOUND"), GH_FILE_STATUS_INVALID("0201", "GIT_HUB_FILE_STATUS_INVALID"), - GH_FILE_TYPE_INVALID("0202", "GIT_HUB_FILE_TYPE_INVALID"); + GH_FILE_TYPE_INVALID("0202", "GIT_HUB_FILE_TYPE_INVALID"), + USER_NOT_FOUND("2103", "USER_NOT_FOUND"), + FEEDBACK_NOT_FOUND("3103", "FEEDBACK_NOT_FOUND"), + ARGUMENT_BAD_REQUEST("4000", "ARGUMENT_BAD_REQUEST"); String code; String helpText; diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java b/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java index d75ca9f54..eb155031f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java @@ -1,11 +1,9 @@ package com.axonivy.market.enums; -import org.apache.commons.lang3.StringUtils; - import com.axonivy.market.exceptions.model.NotFoundException; - import lombok.AllArgsConstructor; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; @Getter @AllArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java b/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java index 75bb5beb9..7703e3cbd 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java @@ -1,11 +1,9 @@ package com.axonivy.market.enums; -import org.apache.commons.lang3.StringUtils; - import com.axonivy.market.exceptions.model.NotFoundException; - import lombok.AllArgsConstructor; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; @Getter @AllArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java index 59914ab90..c3e9714e3 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java @@ -1,11 +1,9 @@ package com.axonivy.market.enums; -import org.apache.commons.lang3.StringUtils; - import com.axonivy.market.exceptions.model.InvalidParamException; - import lombok.AllArgsConstructor; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; @Getter @AllArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java index 3b513ea4a..1c30aca92 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java @@ -1,10 +1,8 @@ package com.axonivy.market.enums; -import org.apache.commons.lang3.StringUtils; - import com.axonivy.market.exceptions.model.InvalidParamException; - import lombok.Getter; +import org.apache.commons.lang3.StringUtils; @Getter public enum TypeOption { diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java index 3dc315608..d9b1ab725 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java @@ -1,19 +1,47 @@ package com.axonivy.market.exceptions; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.exceptions.model.InvalidParamException; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; +import com.axonivy.market.model.Message; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import com.axonivy.market.exceptions.model.InvalidParamException; -import com.axonivy.market.exceptions.model.MissingHeaderException; -import com.axonivy.market.exceptions.model.NotFoundException; -import com.axonivy.market.model.Message; +import java.util.ArrayList; +import java.util.List; @ControllerAdvice public class ExceptionHandlers extends ResponseEntityExceptionHandler { + @Override + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + BindingResult bindingResult = ex.getBindingResult(); + List errors = new ArrayList<>(); + if (bindingResult.hasErrors()) { + for (FieldError fieldError : bindingResult.getFieldErrors()) { + errors.add(fieldError.getDefaultMessage()); + } + } else { + errors.add(ex.getMessage()); + } + + var errorMessage = new Message(); + errorMessage.setHelpCode(ErrorCode.ARGUMENT_BAD_REQUEST.getCode()); + errorMessage.setMessageDetails(ErrorCode.ARGUMENT_BAD_REQUEST.getHelpText() + " - " + String.join("; ", errors)); + return new ResponseEntity<>(errorMessage, status); + } + @ExceptionHandler(MissingHeaderException.class) public ResponseEntity handleMissingServletRequestParameter(MissingHeaderException missingHeaderException) { var errorMessage = new Message(); @@ -36,4 +64,12 @@ public ResponseEntity handleInvalidException(InvalidParamException inval errorMessage.setMessageDetails(invalidDataException.getMessage()); return new ResponseEntity<>(errorMessage, HttpStatus.BAD_REQUEST); } + + @ExceptionHandler(Oauth2ExchangeCodeException.class) + public ResponseEntity handleOauth2ExchangeCodeException(Oauth2ExchangeCodeException oauth2ExchangeCodeException) { + var errorMessage = new Message(); + errorMessage.setHelpCode(oauth2ExchangeCodeException.getError()); + errorMessage.setMessageDetails(oauth2ExchangeCodeException.getErrorDescription()); + return new ResponseEntity<>(errorMessage, HttpStatus.BAD_REQUEST); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java index 8a82188fa..3cecdf03e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java @@ -1,7 +1,6 @@ package com.axonivy.market.exceptions.model; import com.axonivy.market.enums.ErrorCode; - import lombok.Getter; import lombok.Setter; diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java index 4b5b158c6..2b6978f7f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java @@ -1,9 +1,12 @@ package com.axonivy.market.exceptions.model; +import java.io.Serial; + import static com.axonivy.market.constants.ErrorMessageConstants.INVALID_MISSING_HEADER_ERROR_MESSAGE; public class MissingHeaderException extends Exception { + @Serial private static final long serialVersionUID = 1L; public MissingHeaderException() { diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java index e1c917749..e84dd9c01 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java @@ -1,16 +1,18 @@ package com.axonivy.market.exceptions.model; import com.axonivy.market.enums.ErrorCode; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; +import java.io.Serial; + @Getter @Setter @AllArgsConstructor public class NotFoundException extends RuntimeException { + @Serial private static final long serialVersionUID = 1L; private static final String SEPARATOR = "-"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java new file mode 100644 index 000000000..d48a88770 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java @@ -0,0 +1,19 @@ +package com.axonivy.market.exceptions.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serial; + +@Getter +@Setter +@AllArgsConstructor +public class Oauth2ExchangeCodeException extends RuntimeException { + + @Serial + private static final long serialVersionUID = 6778659816121728814L; + + private String error; + private String errorDescription; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java index 1bde19a0e..f9ff5a69f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubFile.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubFile.java index 9586f0886..ca5d5ba70 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubFile.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubFile.java @@ -1,14 +1,13 @@ package com.axonivy.market.github.model; -import java.util.Date; - import com.axonivy.market.enums.FileStatus; import com.axonivy.market.enums.FileType; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.Date; + @Getter @Setter @NoArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java index 92e940487..918b90378 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java @@ -1,16 +1,15 @@ package com.axonivy.market.github.model; -import java.util.List; - import com.axonivy.market.model.DisplayValue; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.List; + @Getter @Setter @NoArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java index a669fac2a..8a3cb88f3 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java @@ -1,13 +1,12 @@ package com.axonivy.market.github.service; -import java.util.List; -import java.util.Map; - +import com.axonivy.market.github.model.GitHubFile; import org.kohsuke.github.GHCommit; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHRepository; -import com.axonivy.market.github.model.GitHubFile; +import java.util.List; +import java.util.Map; public interface GHAxonIvyMarketRepoService { diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java index 7dd5009db..5cf1a9d01 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java @@ -1,22 +1,28 @@ package com.axonivy.market.github.service; -import java.io.IOException; -import java.util.List; - +import com.axonivy.market.entity.User; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; +import java.io.IOException; +import java.util.List; +import java.util.Map; + public interface GitHubService { - public GitHub getGitHub() throws IOException; + GitHub getGitHub() throws IOException; + + GHOrganization getOrganization(String orgName) throws IOException; + + GHRepository getRepository(String repositoryPath) throws IOException; - public GHOrganization getOrganization(String orgName) throws IOException; + List getDirectoryContent(GHRepository ghRepository, String path, String ref) throws IOException; - public GHRepository getRepository(String repositoryPath) throws IOException; + GHContent getGHContent(GHRepository ghRepository, String path, String ref) throws IOException; - public List getDirectoryContent(GHRepository ghRepository, String path, String ref) throws IOException; + Map getAccessToken(String code, String clientId, String clientSecret); - public GHContent getGHContent(GHRepository ghRepository, String path, String ref) throws IOException; + User getAndUpdateUser(String accessToken); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java index 6c96bee26..61aca55a9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java @@ -1,21 +1,5 @@ package com.axonivy.market.github.service.impl; -import java.io.IOException; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.kohsuke.github.GHCommit; -import org.kohsuke.github.GHCommitQueryBuilder; -import org.kohsuke.github.GHCompare; -import org.kohsuke.github.GHContent; -import org.kohsuke.github.GHOrganization; -import org.kohsuke.github.GHRepository; -import org.springframework.stereotype.Service; - import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.enums.FileStatus; import com.axonivy.market.enums.FileType; @@ -23,8 +7,17 @@ import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; import com.axonivy.market.github.service.GitHubService; import com.axonivy.market.github.util.GitHubUtils; - import lombok.extern.log4j.Log4j2; +import org.kohsuke.github.*; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @Log4j2 @Service diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java index 62bbd9181..6af97d1fa 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java @@ -1,26 +1,40 @@ package com.axonivy.market.github.service.impl; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.util.List; - -import org.kohsuke.github.GHContent; -import org.kohsuke.github.GHOrganization; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.GitHub; -import org.kohsuke.github.GitHubBuilder; +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.repository.UserRepository; +import org.kohsuke.github.*; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.ResourceUtils; +import org.springframework.web.client.RestTemplate; -import com.axonivy.market.github.service.GitHubService; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Collections; +import java.util.List; +import java.util.Map; @Service public class GitHubServiceImpl implements GitHubService { + private final RestTemplate restTemplate; + private final UserRepository userRepository; + private static final String GITHUB_TOKEN_FILE = "classpath:github.token"; + public GitHubServiceImpl(RestTemplateBuilder restTemplateBuilder, UserRepository userRepository) { + this.restTemplate = restTemplateBuilder.build(); + this.userRepository = userRepository; + } + @Override public GitHub getGitHub() throws IOException { File gitHubToken = ResourceUtils.getFile(GITHUB_TOKEN_FILE); @@ -49,4 +63,57 @@ public GHContent getGHContent(GHRepository ghRepository, String path, String ref Assert.notNull(ghRepository, "Repository must not be null"); return ghRepository.getFileContent(path, ref); } + + @Override + public Map getAccessToken(String code, String clientId, String clientSecret) throws Oauth2ExchangeCodeException { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add(GitHubConstants.Json.CLIENT_ID, clientId); + params.add(GitHubConstants.Json.CLIENT_SECRET, clientSecret); + params.add(GitHubConstants.Json.CODE, code); + + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + HttpEntity> request = new HttpEntity<>(params, headers); + + ResponseEntity response = restTemplate.postForEntity(GitHubConstants.GITHUB_GET_ACCESS_TOKEN_URL, request, Map.class); + if (response.getBody().containsKey(GitHubConstants.Json.ERROR)) { + throw new Oauth2ExchangeCodeException(response.getBody().get(GitHubConstants.Json.ERROR).toString(), response.getBody().get(GitHubConstants.Json.ERROR_DESCRIPTION).toString()); + } + return response.getBody(); + } + + @Override + public User getAndUpdateUser(String accessToken) { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange( + "https://api.github.com/user", HttpMethod.GET, entity, Map.class); + + Map userDetails = response.getBody(); + + if (userDetails == null) { + throw new RuntimeException("Failed to fetch user details from GitHub"); + } + + String gitHubId = userDetails.get(GitHubConstants.Json.USER_ID).toString(); + String name = (String) userDetails.get(GitHubConstants.Json.USER_NAME); + String avatarUrl = (String) userDetails.get(GitHubConstants.Json.USER_AVATAR_URL); + String username = (String) userDetails.get(GitHubConstants.Json.USER_LOGIN_NAME); + + User user = userRepository.searchByGitHubId(gitHubId); + if (user == null) { + user = new User(); + } + user.setGitHubId(gitHubId); + user.setName(name); + user.setUsername(username); + user.setAvatarUrl(avatarUrl); + user.setProvider(GitHubConstants.GITHUB_PROVIDER_NAME); + + userRepository.save(user); + + return user; + } } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java b/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java index cd0bf4abf..70d54b588 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java @@ -1,15 +1,13 @@ package com.axonivy.market.model; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; - import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; @Getter @Setter diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java new file mode 100644 index 000000000..5eb1769ce --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java @@ -0,0 +1,42 @@ +package com.axonivy.market.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.hateoas.server.core.Relation; + +import java.util.Date; + +@Getter +@Setter +@NoArgsConstructor +@Relation(collectionRelation = "feedbacks", itemRelation = "feedback") +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FeedbackModel extends RepresentationModel { + private String id; + private String username; + private String userAvatarUrl; + private String userProvider; + private String productId; + private String content; + private Integer rating; + private Date createdAt; + private Date updatedAt; + + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return new EqualsBuilder().append(id, ((FeedbackModel) obj).getId()).isEquals(); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java b/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java index 58431cf8e..389c4832e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java @@ -1,11 +1,11 @@ package com.axonivy.market.model; -import java.io.Serializable; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.io.Serializable; + @Getter @Setter @NoArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java b/marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java new file mode 100644 index 000000000..56706c4c1 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java @@ -0,0 +1,12 @@ +package com.axonivy.market.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class Oauth2AuthorizationCode { + public String code; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java index 7ba586438..0984f8765 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java @@ -1,18 +1,16 @@ package com.axonivy.market.model; -import java.util.List; - -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.springframework.hateoas.RepresentationModel; -import org.springframework.hateoas.server.core.Relation; - import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.hateoas.server.core.Relation; + +import java.util.List; @Getter @Setter diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ProductRating.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductRating.java new file mode 100644 index 000000000..b151e05f4 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductRating.java @@ -0,0 +1,16 @@ +package com.axonivy.market.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class ProductRating { + private Integer starRating; + private Integer commentNumber; + private Integer percent; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/FeedbackRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/FeedbackRepository.java new file mode 100644 index 000000000..bbb1fc301 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/FeedbackRepository.java @@ -0,0 +1,21 @@ +package com.axonivy.market.repository; + +import com.axonivy.market.entity.Feedback; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface FeedbackRepository extends MongoRepository { + + @Query("{ 'productId': ?0 }") + Page searchByProductId(String productId, Pageable pageable); + + List findByProductId(String productId); + + Feedback findByUserIdAndProductId(String userId, String productId); +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java index 49424d63c..6dda95549 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java @@ -1,8 +1,7 @@ package com.axonivy.market.repository; -import org.springframework.data.mongodb.repository.MongoRepository; - import com.axonivy.market.entity.GitHubRepoMeta; +import org.springframework.data.mongodb.repository.MongoRepository; public interface GitHubRepoMetaRepository extends MongoRepository { diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java index 7fabd79bc..b638e30ea 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java @@ -1,13 +1,12 @@ package com.axonivy.market.repository; +import com.axonivy.market.entity.Product; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.Query; import org.springframework.stereotype.Repository; -import com.axonivy.market.entity.Product; - import java.util.Optional; @Repository diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java index 6a011e9c6..30969ab97 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java @@ -6,4 +6,5 @@ @Repository public interface UserRepository extends MongoRepository { + User searchByGitHubId(String gitHubId); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java b/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java index 96153ba39..5621c2d84 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java +++ b/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java @@ -1,11 +1,9 @@ package com.axonivy.market.schedulingtask; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - import com.axonivy.market.service.ProductService; - import lombok.extern.log4j.Log4j2; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; @Log4j2 @Component diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java b/marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java new file mode 100644 index 000000000..1e8988f22 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java @@ -0,0 +1,17 @@ +package com.axonivy.market.service; + +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.ProductRating; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface FeedbackService { + Page findFeedbacks(String productId, Pageable pageable) throws NotFoundException; + Feedback findFeedback(String id) throws NotFoundException; + Feedback findFeedbackByUserIdAndProductId(String userId, String productId) throws NotFoundException; + Feedback upsertFeedback(Feedback feedback) throws NotFoundException; + List getProductRatingById(String productId); +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java b/marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java new file mode 100644 index 000000000..49f1c9d44 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java @@ -0,0 +1,10 @@ +package com.axonivy.market.service; + +import com.axonivy.market.entity.User; +import io.jsonwebtoken.Claims; + +public interface JwtService { + String generateToken(User user); + boolean validateToken(String token); + Claims getClaimsFromToken(String token); +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java index b90604d42..a44f60668 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java @@ -1,10 +1,9 @@ package com.axonivy.market.service; +import com.axonivy.market.entity.Product; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import com.axonivy.market.entity.Product; - public interface ProductService { Page findProducts(String type, String keyword, String language, Pageable pageable); diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java b/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java index 99c08f031..b6c064b4f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java @@ -1,9 +1,12 @@ package com.axonivy.market.service; -import java.util.List; - import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.NotFoundException; + +import java.util.List; public interface UserService { List getAllUsers(); + User createUser(User user); + User findUser(String id) throws NotFoundException; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java new file mode 100644 index 000000000..74ae9a998 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java @@ -0,0 +1,102 @@ +package com.axonivy.market.service.impl; + +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.ProductRating; +import com.axonivy.market.repository.FeedbackRepository; +import com.axonivy.market.repository.ProductRepository; +import com.axonivy.market.repository.UserRepository; +import com.axonivy.market.service.FeedbackService; +import com.axonivy.market.service.UserService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Service +public class FeedbackServiceImpl implements FeedbackService { + + private final FeedbackRepository feedbackRepository; + private final UserRepository userRepository; + private final ProductRepository productRepository; + + public FeedbackServiceImpl(FeedbackRepository feedbackRepository, UserRepository userRepository, ProductRepository productRepository, UserService userService) { + this.feedbackRepository = feedbackRepository; + this.userRepository = userRepository; + this.productRepository = productRepository; + } + + @Override + public Page findFeedbacks(String productId, Pageable pageable) throws NotFoundException { + validateProductExists(productId); + return feedbackRepository.searchByProductId(productId, pageable); + } + + @Override + public Feedback findFeedback(String id) throws NotFoundException { + return feedbackRepository.findById(id).orElseThrow(() -> new NotFoundException(ErrorCode.FEEDBACK_NOT_FOUND, "Not found feedback with id: " + id)); + } + + @Override + public Feedback findFeedbackByUserIdAndProductId(String userId, String productId) throws NotFoundException { + userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND, "Not found user with id: " + userId)); + validateProductExists(productId); + + Feedback existingUserFeedback = feedbackRepository.findByUserIdAndProductId(userId, productId); + if (existingUserFeedback == null) { + throw new NotFoundException(ErrorCode.FEEDBACK_NOT_FOUND, String.format("Not found feedback with user id '%s' and product id '%s'", userId, productId)); + } + return existingUserFeedback; + } + + @Override + public Feedback upsertFeedback(Feedback feedback) throws NotFoundException { + userRepository.findById(feedback.getUserId()) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND,"Not found user with id: " + feedback.getUserId())); + + Feedback existingUserFeedback = feedbackRepository.findByUserIdAndProductId(feedback.getUserId(), feedback.getProductId()); + if (existingUserFeedback == null) { + return feedbackRepository.save(feedback); + } else { + existingUserFeedback.setRating(feedback.getRating()); + existingUserFeedback.setContent(feedback.getContent()); + return feedbackRepository.save(existingUserFeedback); + } + } + + @Override + public List getProductRatingById(String productId) { + List feedbacks = feedbackRepository.findByProductId(productId); + int totalFeedbacks = feedbacks.size(); + + if (totalFeedbacks == 0) { + return IntStream.rangeClosed(1, 5) + .mapToObj(star -> new ProductRating(star, 0, 0)) + .collect(Collectors.toList()); + } + + Map ratingCountMap = feedbacks.stream() + .collect(Collectors.groupingBy(Feedback::getRating, Collectors.counting())); + + return IntStream.rangeClosed(1, 5) + .mapToObj(star -> { + long count = ratingCountMap.getOrDefault(star, 0L); + int percent = (int) ((count * 100) / totalFeedbacks); + return new ProductRating(star, Math.toIntExact(count), percent); + }) + .collect(Collectors.toList()); + } + + private void validateProductExists(String productId) throws NotFoundException { + productRepository.findById(productId) + .orElseThrow(() -> new NotFoundException(ErrorCode.PRODUCT_NOT_FOUND, "Not found product with id: " + productId)); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java new file mode 100644 index 000000000..979bec72c --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java @@ -0,0 +1,54 @@ +package com.axonivy.market.service.impl; + +import com.axonivy.market.entity.User; +import com.axonivy.market.service.JwtService; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Component +public class JwtServiceImpl implements JwtService { + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration}") + private long expiration; + + public String generateToken(User user) { + Map claims = new HashMap<>(); + claims.put("name", user.getName()); + claims.put("username", user.getUsername()); + return Jwts.builder() + .setClaims(claims) + .setSubject(user.getId()) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + expiration * 86400000)) + .signWith(SignatureAlgorithm.HS512, secret) + .compact(); + } + + public boolean validateToken(String token) { + try { + getClaimsJws(token); + return true; + } catch (Exception e) { + return false; + } + } + + public Claims getClaimsFromToken(String token) { + return getClaimsJws(token).getBody(); + } + + public Jws getClaimsJws(String token) { + return Jwts.parser().setSigningKey(secret).parseClaimsJws(token); + } +} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java index dc9990949..750bf3995 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java @@ -1,12 +1,13 @@ package com.axonivy.market.service.impl; -import java.util.List; - -import org.springframework.stereotype.Service; - import com.axonivy.market.entity.User; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.exceptions.model.NotFoundException; import com.axonivy.market.repository.UserRepository; import com.axonivy.market.service.UserService; +import org.springframework.stereotype.Service; + +import java.util.List; @Service public class UserServiceImpl implements UserService { @@ -22,4 +23,13 @@ public List getAllUsers() { return userRepository.findAll(); } + @Override + public User findUser(String id) throws NotFoundException { + return userRepository.findById(id).orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND, "Not found user with id: " + id)); + } + + @Override + public User createUser(User user) { + return userRepository.save(user); + } } diff --git a/marketplace-service/src/main/resources/application.properties b/marketplace-service/src/main/resources/application.properties index 458102046..ea43ce9a9 100644 --- a/marketplace-service/src/main/resources/application.properties +++ b/marketplace-service/src/main/resources/application.properties @@ -9,3 +9,7 @@ springdoc.api-docs.path=/api-docs springdoc.swagger-ui.path=/swagger-ui.html market.cors.allowed.origin.patterns=http://localhost:[*], http://10.193.8.78:[*], http://marketplace.server.ivy-cloud.com:[*] market.cors.allowed.origin.maxAge=3600 +spring.security.oauth2.client.registration.github.client-id=Ov23liUzb36JCQIfEBGn +spring.security.oauth2.client.registration.github.client-secret=d57a58cdc87bc9301d45fde3e2bdf3bff22fcbe1 +jwt.secret=dc4de2d13eaa5be9c185a8814c3afeac36440b19e0955aa7a5eecb7aa27b4fa4 +jwt.expiration=365 diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/AppControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/AppControllerTest.java index 104006d9d..055b81d2c 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/AppControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/AppControllerTest.java @@ -1,14 +1,14 @@ package com.axonivy.market.controller; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.springframework.http.HttpStatus; import org.springframework.test.context.junit.jupiter.SpringExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + @ExtendWith(SpringExtension.class) class AppControllerTest { diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java new file mode 100644 index 000000000..55817625b --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java @@ -0,0 +1,161 @@ +package com.axonivy.market.controller; + +import com.axonivy.market.assembler.FeedbackModelAssembler; +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.entity.User; +import com.axonivy.market.service.FeedbackService; +import com.axonivy.market.service.JwtService; +import com.axonivy.market.service.UserService; +import io.jsonwebtoken.Claims; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.hateoas.PagedModel; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class FeedbackControllerTest { + + private static final String PRODUCT_ID_SAMPLE = "product-id"; + private static final String FEEDBACK_ID_SAMPLE = "feedback-id"; + private static final String USER_ID_SAMPLE = "user-id"; + private static final String USER_NAME_SAMPLE = "Test User"; + private static final String TOKEN_SAMPLE = "token-sample"; + + @Mock + private FeedbackService service; + + @Mock + private JwtService jwtService; + + @Mock + private UserService userService; + + @Mock + private FeedbackModelAssembler feedbackModelAssembler; + + @Mock + private PagedResourcesAssembler pagedResourcesAssembler; + + @InjectMocks + private FeedbackController feedbackController; + + @BeforeEach + void setup() { + feedbackModelAssembler = new FeedbackModelAssembler(userService); + feedbackController = new FeedbackController(service, jwtService, feedbackModelAssembler, pagedResourcesAssembler); + } + + @Test + void testFindFeedbacksAsEmpty() { + PageRequest pageable = PageRequest.of(0, 20); + Page mockFeedbacks = new PageImpl<>(List.of(), pageable, 0); + when(service.findFeedbacks(any(), any())).thenReturn(mockFeedbacks); + when(pagedResourcesAssembler.toEmptyModel(any(), any())).thenReturn(PagedModel.empty()); + var result = feedbackController.findFeedbacks(PRODUCT_ID_SAMPLE, pageable); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertTrue(result.hasBody()); + assertEquals(0, result.getBody().getContent().size()); + } + + @Test + void testFindFeedbacks() { + PageRequest pageable = PageRequest.of(0, 20); + Feedback mockFeedback = createFeedbackMock(); + User mockUser = createUserMock(); + + Page mockFeedbacks = new PageImpl<>(List.of(mockFeedback), pageable, 1); + when(service.findFeedbacks(any(), any())).thenReturn(mockFeedbacks); + when(userService.findUser(any())).thenReturn(mockUser); + var mockFeedbackModel = feedbackModelAssembler.toModel(mockFeedback); + var mockPagedModel = PagedModel.of(List.of(mockFeedbackModel), new PagedModel.PageMetadata(1, 0, 1)); + when(pagedResourcesAssembler.toModel(any(), any(FeedbackModelAssembler.class))).thenReturn(mockPagedModel); + var result = feedbackController.findFeedbacks(PRODUCT_ID_SAMPLE, pageable); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertTrue(result.hasBody()); + assertEquals(1, result.getBody().getContent().size()); + assertEquals(USER_NAME_SAMPLE, result.getBody().getContent().iterator().next().getUsername()); + } + + @Test + void testFindFeedback() { + Feedback mockFeedback = createFeedbackMock(); + User mockUser = createUserMock(); + when(service.findFeedback(FEEDBACK_ID_SAMPLE)).thenReturn(mockFeedback); + when(userService.findUser(any())).thenReturn(mockUser); + var result = feedbackController.findFeedback(FEEDBACK_ID_SAMPLE); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertTrue(result.hasBody()); + assertEquals(USER_NAME_SAMPLE, result.getBody().getUsername()); + } + + @Test + void testFindFeedbackByUserIdAndProductId() { + Feedback mockFeedback = createFeedbackMock(); + User mockUser = createUserMock(); + when(service.findFeedbackByUserIdAndProductId(any(), any())).thenReturn(mockFeedback); + when(userService.findUser(any())).thenReturn(mockUser); + var result = feedbackController.findFeedbackByUserIdAndProductId(USER_ID_SAMPLE, PRODUCT_ID_SAMPLE); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertTrue(result.hasBody()); + assertEquals(USER_NAME_SAMPLE, result.getBody().getUsername()); + } + + @Test + void testCreateFeedback() { + Feedback mockFeedback = createFeedbackMock(); + Claims mockClaims = createMockClaims(); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + when(jwtService.validateToken(TOKEN_SAMPLE)).thenReturn(true); + when(jwtService.getClaimsFromToken(TOKEN_SAMPLE)).thenReturn(mockClaims); + when(service.upsertFeedback(any())).thenReturn(mockFeedback); + + var result = feedbackController.createFeedback(mockFeedback, "Bearer " + TOKEN_SAMPLE); + assertEquals(HttpStatus.CREATED, result.getStatusCode()); + assertTrue(result.getHeaders().getLocation().toString().contains(mockFeedback.getId())); + } + + private Feedback createFeedbackMock() { + Feedback mockFeedback = new Feedback(); + mockFeedback.setId(FEEDBACK_ID_SAMPLE); + mockFeedback.setUserId(USER_ID_SAMPLE); + mockFeedback.setProductId(PRODUCT_ID_SAMPLE); + mockFeedback.setContent("Great product!"); + mockFeedback.setRating(5); + return mockFeedback; + } + + private User createUserMock() { + User mockUser = new User(); + mockUser.setId(USER_ID_SAMPLE); + mockUser.setUsername("testUser"); + mockUser.setName("Test User"); + mockUser.setAvatarUrl("http://avatar.url"); + mockUser.setProvider("local"); + return mockUser; + } + + private Claims createMockClaims() { + Claims claims = new io.jsonwebtoken.impl.DefaultClaims(); + claims.setSubject(USER_ID_SAMPLE); + return claims; + } +} \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java new file mode 100644 index 000000000..eff891be0 --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java @@ -0,0 +1,66 @@ +package com.axonivy.market.controller; + +import com.axonivy.market.entity.User; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.Oauth2AuthorizationCode; +import com.axonivy.market.service.JwtService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class OAuth2ControllerTest { + + @Mock + private GitHubService gitHubService; + + @Mock + private JwtService jwtService; + + @InjectMocks + private OAuth2Controller oAuth2Controller; + + private Oauth2AuthorizationCode oauth2AuthorizationCode; + + @BeforeEach + void setup() { + oauth2AuthorizationCode = new Oauth2AuthorizationCode(); + oauth2AuthorizationCode.setCode("sampleCode"); + } + + @Test + void testGitHubLogin() { + String accessToken = "sampleAccessToken"; + User user = createUserMock(); + String jwtToken = "sampleJwtToken"; + + when(gitHubService.getAccessToken(any(), any(), any())).thenReturn(Map.of("access_token", accessToken)); + when(gitHubService.getAndUpdateUser(accessToken)).thenReturn(user); + when(jwtService.generateToken(user)).thenReturn(jwtToken); + + ResponseEntity response = oAuth2Controller.gitHubLogin(oauth2AuthorizationCode); + + assertEquals(200, response.getStatusCodeValue()); + assertEquals(Map.of("token", jwtToken), response.getBody()); + } + + private User createUserMock() { + User user = new User(); + user.setId("userId"); + user.setUsername("username"); + user.setName("User Name"); + user.setAvatarUrl("http://avatar.url"); + user.setProvider("github"); + return user; + } +} \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java index 00417f662..0d984281e 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java @@ -1,12 +1,13 @@ package com.axonivy.market.controller; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import java.util.List; - +import com.axonivy.market.assembler.ProductModelAssembler; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.enums.SortOption; +import com.axonivy.market.enums.TypeOption; +import com.axonivy.market.model.MultilingualismValue; +import com.axonivy.market.model.ProductRating; +import com.axonivy.market.service.ProductService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,16 +24,16 @@ import org.springframework.hateoas.PagedModel.PageMetadata; import org.springframework.http.HttpStatus; -import com.axonivy.market.assembler.ProductModelAssembler; -import com.axonivy.market.entity.Product; -import com.axonivy.market.enums.ErrorCode; -import com.axonivy.market.enums.SortOption; -import com.axonivy.market.enums.TypeOption; -import com.axonivy.market.model.MultilingualismValue; -import com.axonivy.market.service.ProductService; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ProductControllerTest { + private static final String PRODUCT_ID_SAMPLE = "amazon-comprehend"; private static final String PRODUCT_NAME_SAMPLE = "Amazon Comprehend"; private static final String PRODUCT_NAME_DE_SAMPLE = "Amazon Comprehend DE"; private static final String PRODUCT_DESC_SAMPLE = "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data."; @@ -112,4 +113,12 @@ private Product createProductMock() { mockProduct.setTags(List.of("AI")); return mockProduct; } -} \ No newline at end of file + + private ProductRating createProductRatingMock() { + ProductRating productRatingMock = new ProductRating(); + productRatingMock.setStarRating(1); + productRatingMock.setPercent(10); + productRatingMock.setCommentNumber(5); + return productRatingMock; + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/UserControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/UserControllerTest.java deleted file mode 100644 index 5886b6473..000000000 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/UserControllerTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.axonivy.market.controller; - -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.axonivy.market.service.UserService; - -@ExtendWith(MockitoExtension.class) -class UserControllerTest { - - @Mock - UserService userService; - - @InjectMocks - UserController userController; - - @Test - void testGetAllUser() { - var result = userController.getAllUser(); - assertNotEquals(null, result); - } -} diff --git a/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java b/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java index 3bbe6f80b..ad5db1617 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java @@ -1,9 +1,10 @@ package com.axonivy.market.handler; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - +import com.axonivy.market.exceptions.ExceptionHandlers; +import com.axonivy.market.exceptions.model.InvalidParamException; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.Message; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,11 +12,9 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; -import com.axonivy.market.exceptions.ExceptionHandlers; -import com.axonivy.market.exceptions.model.InvalidParamException; -import com.axonivy.market.exceptions.model.MissingHeaderException; -import com.axonivy.market.exceptions.model.NotFoundException; -import com.axonivy.market.model.Message; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ExceptionHandlersTest { diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java new file mode 100644 index 000000000..51a510357 --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java @@ -0,0 +1,189 @@ +package com.axonivy.market.service; + +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.ProductRating; +import com.axonivy.market.repository.FeedbackRepository; +import com.axonivy.market.repository.ProductRepository; +import com.axonivy.market.repository.UserRepository; +import com.axonivy.market.service.impl.FeedbackServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Pageable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class FeedbackServiceImplTest { + + @Mock + private FeedbackRepository feedbackRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private ProductRepository productRepository; + + @InjectMocks + private FeedbackServiceImpl feedbackService; + + @BeforeEach + void setUp() { + // Mock initialization or setup if needed + } + + @Test + void testFindFeedbacks_ProductNotFound() { + String productId = "nonExistingProduct"; + + when(productRepository.findById(productId)).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> feedbackService.findFeedbacks(productId, Pageable.unpaged())); + + verify(productRepository, times(1)).findById(productId); + verify(feedbackRepository, never()).searchByProductId(any(), any()); + } + + @Test + void testFindFeedback_Success() throws NotFoundException { + // Mock data + String feedbackId = "feedback123"; + Feedback mockFeedback = new Feedback(); + mockFeedback.setId(feedbackId); + + // Mock behavior + when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.of(mockFeedback)); + + // Test method + Feedback result = feedbackService.findFeedback(feedbackId); + + // Verify + assertEquals(mockFeedback, result); + verify(feedbackRepository, times(1)).findById(feedbackId); + } + + @Test + void testFindFeedback_NotFound() { + // Mock data + String nonExistingId = "nonExistingFeedbackId"; + + // Mock behavior + when(feedbackRepository.findById(nonExistingId)).thenReturn(Optional.empty()); + + // Test and verify exception + assertThrows(NotFoundException.class, () -> feedbackService.findFeedback(nonExistingId)); + + // Verify interactions + verify(feedbackRepository, times(1)).findById(nonExistingId); + } + + @Test + void testFindFeedbackByUserIdAndProductId_UserNotFound() { + // Mock data + String nonExistingUserId = "nonExistingUser"; + String productId = "product123"; + + // Mock behavior + when(userRepository.findById(nonExistingUserId)).thenReturn(Optional.empty()); + + // Test and verify exception + assertThrows(NotFoundException.class, () -> feedbackService.findFeedbackByUserIdAndProductId(nonExistingUserId, productId)); + + // Verify interactions + verify(userRepository, times(1)).findById(nonExistingUserId); + verify(feedbackRepository, never()).findByUserIdAndProductId(any(), any()); + } + + @Test + void testUpsertFeedback_NewFeedback() throws NotFoundException { + // Mock data + Feedback newFeedback = new Feedback(); + newFeedback.setUserId("user123"); + newFeedback.setProductId("product123"); + newFeedback.setContent("Great product!"); + newFeedback.setRating(5); + + User u = new User(); + u.setId(newFeedback.getUserId()); + when(userRepository.findById(newFeedback.getUserId())).thenReturn(Optional.of(u)); + when(feedbackRepository.findByUserIdAndProductId(newFeedback.getUserId(), newFeedback.getProductId())).thenReturn(null); + when(feedbackRepository.save(newFeedback)).thenReturn(newFeedback); + + // Test method + Feedback result = feedbackService.upsertFeedback(newFeedback); + + // Verify + assertEquals(newFeedback, result); + verify(userRepository, times(1)).findById(newFeedback.getUserId()); + verify(feedbackRepository, times(1)).findByUserIdAndProductId(newFeedback.getUserId(), newFeedback.getProductId()); + verify(feedbackRepository, times(1)).save(newFeedback); + } + + @Test + void testUpsertFeedback_UpdateFeedback() throws NotFoundException { + // Mock data + Feedback existingFeedback = new Feedback(); + existingFeedback.setId("existingFeedback123"); + existingFeedback.setUserId("user123"); + existingFeedback.setProductId("product123"); + existingFeedback.setContent("Good product!"); + existingFeedback.setRating(4); + + User u = new User(); + u.setId(existingFeedback.getUserId()); + when(userRepository.findById(existingFeedback.getUserId())).thenReturn(Optional.of(u)); + when(feedbackRepository.findByUserIdAndProductId(existingFeedback.getUserId(), existingFeedback.getProductId())).thenReturn(existingFeedback); + when(feedbackRepository.save(existingFeedback)).thenReturn(existingFeedback); + + // Test method + Feedback updatedFeedback = new Feedback(); + updatedFeedback.setId(existingFeedback.getId()); + updatedFeedback.setUserId(existingFeedback.getUserId()); + updatedFeedback.setProductId(existingFeedback.getProductId()); + updatedFeedback.setContent("Excellent product!"); + updatedFeedback.setRating(5); + + Feedback result = feedbackService.upsertFeedback(updatedFeedback); + + // Verify + assertEquals(updatedFeedback.getId(), result.getId()); + assertEquals(updatedFeedback.getContent(), result.getContent()); + assertEquals(updatedFeedback.getRating(), result.getRating()); + verify(userRepository, times(1)).findById(existingFeedback.getUserId()); + verify(feedbackRepository, times(1)).findByUserIdAndProductId(existingFeedback.getUserId(), existingFeedback.getProductId()); + verify(feedbackRepository, times(1)).save(existingFeedback); + } + + @Test + void testGetProductRatingById_NoFeedbacks() { + // Mock data + String productId = "product123"; + + // Mock behavior + when(feedbackRepository.findByProductId(productId)).thenReturn(new ArrayList<>()); + + // Test method + List result = feedbackService.getProductRatingById(productId); + + // Verify + assertEquals(5, result.size()); // Expect ratings for stars 1 to 5 + result.forEach(rating -> { + assertEquals(0, rating.getCommentNumber()); + assertEquals(0, rating.getPercent()); + }); + verify(feedbackRepository, times(1)).findByProductId(productId); + } +} + diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java index fa74bc54a..0e1f06f4b 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java @@ -1,32 +1,27 @@ package com.axonivy.market.service; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.github.service.impl.GHAxonIvyMarketRepoServiceImpl; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHCommit.File; -import org.kohsuke.github.GHCompare; +import org.kohsuke.github.*; import org.kohsuke.github.GHCompare.Commit; -import org.kohsuke.github.GHContent; -import org.kohsuke.github.GHOrganization; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.PagedIterable; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.github.service.impl.GHAxonIvyMarketRepoServiceImpl; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class GHAxonIvyMarketRepoServiceImplTest { diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java index e26226c6b..bbd8416fa 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java @@ -1,13 +1,8 @@ package com.axonivy.market.service; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; - import com.axonivy.market.github.service.impl.GitHubServiceImpl; +import com.axonivy.market.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHContent; @@ -15,7 +10,16 @@ import org.kohsuke.github.GitHub; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class GitHubServiceImplTest { @@ -27,9 +31,25 @@ class GitHubServiceImplTest { @Mock GHRepository ghRepository; + @Mock + private RestTemplateBuilder restTemplateBuilder; + + @Mock + private RestTemplate restTemplate; + + @Mock + private UserRepository userRepository; + @InjectMocks private GitHubServiceImpl gitHubService; + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + // Use lenient stubbing + lenient().when(restTemplateBuilder.build()).thenReturn(restTemplate); + } + @Test void testGetGithub() throws IOException { var result = gitHubService.getGitHub(); @@ -51,5 +71,4 @@ void testGetDirectoryContent() throws IOException { var result = gitHubService.getDirectoryContent(ghRepository, "", ""); assertEquals(0, result.size()); } - } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java new file mode 100644 index 000000000..4eb8ffcec --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java @@ -0,0 +1,94 @@ +package com.axonivy.market.service; + +import com.axonivy.market.entity.User; +import com.axonivy.market.service.impl.JwtServiceImpl; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class JwtServiceImplTest { + + private static final String SECRET = "mySecret"; + private static final long EXPIRATION = 7L; // 7 days + + @InjectMocks + private JwtServiceImpl jwtService; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(jwtService, "secret", SECRET); + ReflectionTestUtils.setField(jwtService, "expiration", EXPIRATION); + } + + @Test + void testGenerateToken() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String token = jwtService.generateToken(user); + + assertNotNull(token); + assertFalse(token.isEmpty()); + + Claims claims = jwtService.getClaimsFromToken(token); + assertEquals("123", claims.getSubject()); + assertEquals("John Doe", claims.get("name")); + assertEquals("johndoe", claims.get("username")); + } + + @Test + void testValidateToken() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String validToken = jwtService.generateToken(user); + assertTrue(jwtService.validateToken(validToken)); + + String invalidToken = "invalid.token.here"; + assertFalse(jwtService.validateToken(invalidToken)); + } + + @Test + void testGetClaimsFromToken() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String token = jwtService.generateToken(user); + + Claims claims = jwtService.getClaimsFromToken(token); + assertNotNull(claims); + assertEquals("123", claims.getSubject()); + assertEquals("John Doe", claims.get("name")); + assertEquals("johndoe", claims.get("username")); + } + + @Test + void testGetClaimsJws() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String token = jwtService.generateToken(user); + + Jws claimsJws = jwtService.getClaimsJws(token); + assertNotNull(claimsJws); + assertNotNull(claimsJws.getBody()); + assertEquals("123", claimsJws.getBody().getSubject()); + } +} + diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java index f23d6ea21..8562d8f66 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java @@ -1,15 +1,14 @@ package com.axonivy.market.service; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.verify; - +import com.axonivy.market.schedulingtask.ScheduledTasks; import org.awaitility.Awaitility; import org.awaitility.Durations; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.SpyBean; -import com.axonivy.market.schedulingtask.ScheduledTasks; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; @SpringBootTest class SchedulingTasksTest { diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java index d14aaff8c..dd786a093 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java @@ -1,7 +1,8 @@ package com.axonivy.market.service; -import java.util.List; - +import com.axonivy.market.entity.User; +import com.axonivy.market.repository.UserRepository; +import com.axonivy.market.service.impl.UserServiceImpl; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -10,9 +11,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import com.axonivy.market.entity.User; -import com.axonivy.market.repository.UserRepository; -import com.axonivy.market.service.impl.UserServiceImpl; +import java.util.List; @ExtendWith(MockitoExtension.class) class UserServiceImplTest { @@ -28,8 +27,7 @@ void testFindAllUser() { // Mock data and service User mockUser = new User(); mockUser.setId("123"); - mockUser.setUsername("tvtTest"); - mockUser.setPassword("12345"); + mockUser.setName("tvtTest"); List mockResultReturn = List.of(mockUser); Mockito.when(userRepository.findAll()).thenReturn(mockResultReturn); @@ -39,4 +37,19 @@ void testFindAllUser() { // Verify Assertions.assertEquals(result, mockResultReturn); } + + @Test + void testCreateUser() { + // Mock data + User mockUser = new User(); + mockUser.setId("123"); + mockUser.setName("tvtTest"); + Mockito.when(userRepository.save(mockUser)).thenReturn(mockUser); + + // Exercise + User result = employeeService.createUser(mockUser); + + // Verify + Assertions.assertEquals(result, mockUser); + } } From 8ac647b64e3eccc15d119bf8ddd8b88ba0817f30 Mon Sep 17 00:00:00 2001 From: Khanh Nguyen Date: Fri, 12 Jul 2024 13:43:30 +0700 Subject: [PATCH 2/3] Update github oauth app client --- marketplace-service/src/main/resources/application.properties | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/marketplace-service/src/main/resources/application.properties b/marketplace-service/src/main/resources/application.properties index 458102046..34dc2446b 100644 --- a/marketplace-service/src/main/resources/application.properties +++ b/marketplace-service/src/main/resources/application.properties @@ -9,3 +9,7 @@ springdoc.api-docs.path=/api-docs springdoc.swagger-ui.path=/swagger-ui.html market.cors.allowed.origin.patterns=http://localhost:[*], http://10.193.8.78:[*], http://marketplace.server.ivy-cloud.com:[*] market.cors.allowed.origin.maxAge=3600 +spring.security.oauth2.client.registration.github.client-id=Ov23liVMliBxBqdQ7FnG +spring.security.oauth2.client.registration.github.client-secret=97ee39cd07698bb95ead8b76ba25f2686a6cc7a6 +jwt.secret=dc4de2d13eaa5be9c185a8814c3afeac36440b19e0955aa7a5eecb7aa27b4fa4 +jwt.expiration=365 From f5797b4b43049f391fd94e3850a9589d7a0cb3fe Mon Sep 17 00:00:00 2001 From: Khanh Nguyen Date: Fri, 12 Jul 2024 14:26:06 +0700 Subject: [PATCH 3/3] Update FeedbackController.java --- .../java/com/axonivy/market/controller/FeedbackController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java index 3232f42be..233c94b4e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java @@ -43,7 +43,7 @@ public FeedbackController(FeedbackService feedbackService, JwtService jwtService @Operation(summary = "Find all feedbacks by product id") @GetMapping("/product/{productId}") - public ResponseEntity> findFeedbacks(@PathVariable String productId, Pageable pageable) { + public ResponseEntity> findFeedbacks(@PathVariable("productId") String productId, Pageable pageable) { Page results = feedbackService.findFeedbacks(productId, pageable); if (results.isEmpty()) { return generateEmptyPagedModel();