diff --git a/marketplace-build/config/nginx/dev/nginx.conf b/marketplace-build/config/nginx/dev/nginx.conf index fc02a344c..219fcd3e7 100644 --- a/marketplace-build/config/nginx/dev/nginx.conf +++ b/marketplace-build/config/nginx/dev/nginx.conf @@ -9,6 +9,7 @@ http { server { listen 80; server_name marketplace; + server_tokens off; root /usr/share/nginx/html; index index.html; @@ -36,14 +37,6 @@ http { } # End: Handle for DOCs - # Start: Handle for LIBs - # Workaround for static libs. e.g: https://market.axonivy.com/demos-app/dev/lib/ivy-demos-app.zip - location ~ ^/([^/]+)/([^/]+)/lib/(.*)$ { - alias /usr/share/nginx/html/cache/$1/$2/lib/; - try_files $3 =404; - } - # End: Handle for LIBs - error_page 403 /error-page; } } \ No newline at end of file diff --git a/marketplace-build/config/nginx/nginx.conf b/marketplace-build/config/nginx/nginx.conf index 6d287b4c5..9881bfdbe 100644 --- a/marketplace-build/config/nginx/nginx.conf +++ b/marketplace-build/config/nginx/nginx.conf @@ -6,6 +6,7 @@ http { server { listen 80; server_name marketplace; + server_tokens off; root /usr/share/nginx/html; index index.html; @@ -35,14 +36,6 @@ http { } # End: Handle for DOCs - # Start: Handle for LIBs - # Workaround for static libs. e.g: https://market.axonivy.com/demos-app/dev/lib/ivy-demos-app.zip - location ~ ^/([^/]+)/([^/]+)/lib/(.*)$ { - alias /usr/share/nginx/html/cache/$1/$2/lib/; - try_files $3 =404; - } - # End: Handle for LIBs - error_page 403 /error-page; } } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java index 4b9340518..1732722a6 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java @@ -1,6 +1,7 @@ package com.axonivy.market.assembler; import com.axonivy.market.constants.RequestMappingConstants; +import com.axonivy.market.controller.ImageController; import com.axonivy.market.controller.ProductDetailsController; import com.axonivy.market.entity.Product; import com.axonivy.market.model.ProductDetailModel; @@ -67,6 +68,7 @@ private ProductDetailModel createModel(Product product, String version, String r private void createDetailResource(ProductDetailModel model, Product product) { model.setVendor(product.getVendor()); + model.setVendorUrl(product.getVendorUrl()); model.setNewestReleaseVersion(product.getNewestReleaseVersion()); model.setPlatformReview(product.getPlatformReview()); model.setSourceUrl(product.getSourceUrl()); @@ -78,6 +80,15 @@ private void createDetailResource(ProductDetailModel model, Product product) { model.setCost(product.getCost()); model.setInstallationCount(product.getInstallationCount()); model.setProductModuleContent(ImageUtils.mappingImageForProductModuleContent(product.getProductModuleContent())); + if (StringUtils.isNotBlank(product.getVendorImage())) { + Link vendorLink = linkTo(methodOn(ImageController.class).findImageById(product.getVendorImage())).withSelfRel(); + model.setVendorImage(vendorLink.getHref()); + } + if (StringUtils.isNotBlank(product.getVendorImageDarkMode())) { + Link vendorDarkModeLink = + linkTo(methodOn(ImageController.class).findImageById(product.getVendorImageDarkMode())).withSelfRel(); + model.setVendorImageDarkMode(vendorDarkModeLink.getHref()); + } } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java index 431d7311e..7380b621b 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java @@ -7,4 +7,5 @@ public class ErrorMessageConstants { public static final String INVALID_MISSING_HEADER_ERROR_MESSAGE = "Invalid or missing header"; public static final String CURRENT_CLIENT_ID_MISMATCH_MESSAGE = " Client ID mismatch (Request ID: %s, Server ID: %s)"; + public static final String INVALID_USER_ERROR = "%s - User must be a member of team %s and organization %s"; } 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 a86ebc8c8..3419c083f 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 @@ -6,6 +6,7 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class GitHubConstants { public static final String AXONIVY_MARKET_ORGANIZATION_NAME = "axonivy-market"; + public static final String AXONIVY_MARKET_TEAM_NAME = "team-octopus"; public static final String AXONIVY_MARKETPLACE_REPO_NAME = "market"; public static final String AXONIVY_MARKETPLACE_PATH = "market"; public static final String GITHUB_PROVIDER_NAME = "GitHub"; @@ -13,6 +14,8 @@ public class GitHubConstants { public static final String README_FILE_LOCALE_REGEX = "_(..)"; public static final String STANDARD_TAG_PREFIX = "v"; public static final String COMMON_IMAGES_FOLDER_NAME = "images"; + public static final String MS_GRAPH_PRODUCT_DIRECTORY = "msgraph-connector-product"; + public static final String MG_GRAPH_IMAGES_FOR_SETUP_FILE = "doc"; @NoArgsConstructor(access = AccessLevel.PRIVATE) public static class Json { @@ -30,6 +33,5 @@ public static class Json { public static class Url { private static final String BASE_URL = "https://api.github.com"; public static final String USER = BASE_URL + "/user"; - public static final String USER_ORGS = USER + "/orgs"; } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java index 2f4e4f946..d2e11abd7 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java @@ -10,4 +10,5 @@ public class ReadmeConstants { public static final String README_FILE_NAME = "README"; public static final String DEMO_PART = "## Demo"; public static final String SETUP_PART = "## Setup"; + public static final String SETUP_FILE = "setup.md"; } 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 89fc726bb..61aa1627d 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 @@ -14,6 +14,7 @@ public class RequestMappingConstants { public static final String IMAGE = API + "/image"; public static final String SYNC = "sync"; public static final String SYNC_PRODUCT_VERSION = SYNC + "/product-version"; + public static final String SYNC_ONE_PRODUCT_BY_ID = "sync/{id}"; public static final String SWAGGER_URL = "/swagger-ui/index.html"; public static final String GIT_HUB_LOGIN = "/github/login"; public static final String AUTH = "/auth"; @@ -28,5 +29,6 @@ public class RequestMappingConstants { public static final String VERSIONS_IN_DESIGNER = "/{id}/designerversions"; public static final String DESIGNER_INSTALLATION_BY_ID = "/installation/{id}/designer"; public static final String CUSTOM_SORT = "custom-sort"; + public static final String LATEST_ARTIFACT_DOWNLOAD_URL_BY_ID = "/{id}/artifact"; public static final String EXTERNAL_DOCUMENT = API + "/externaldocument"; -} \ No newline at end of file +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java index b1fff6169..516652a5f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java @@ -16,4 +16,8 @@ public class RequestParamConstants { public static final String SHOW_DEV_VERSION = "isShowDevVersion"; public static final String DESIGNER_VERSION = "designerVersion"; public static final String VERSION = "version"; + public static final String ARTIFACT = "artifact"; + public static final String MARKET_ITEM_PATH = "marketItemPath"; + public static final String OVERRIDE_MARKET_ITEM_PATH = "overrideMarketItemPath"; + } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ExternalDocumentController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ExternalDocumentController.java index bd9d2eb59..ac8d850b7 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ExternalDocumentController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ExternalDocumentController.java @@ -1,9 +1,11 @@ package com.axonivy.market.controller; import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.ExternalDocumentMeta; import com.axonivy.market.entity.Product; import com.axonivy.market.enums.ErrorCode; import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.ExternalDocumentModel; import com.axonivy.market.model.Message; import com.axonivy.market.service.ExternalDocumentService; import com.axonivy.market.util.AuthorizationUtils; @@ -13,7 +15,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -24,12 +25,12 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.net.URI; -import java.net.URISyntaxException; import java.util.List; import static com.axonivy.market.constants.RequestMappingConstants.*; import static com.axonivy.market.constants.RequestParamConstants.*; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; import static org.springframework.http.HttpHeaders.AUTHORIZATION; @RestController @@ -41,16 +42,22 @@ public class ExternalDocumentController { final GitHubService gitHubService; @GetMapping(BY_ID_AND_VERSION) - public ResponseEntity findExternalDocumentURI( + public ResponseEntity findExternalDocument( @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "portal", in = ParameterIn.PATH) String id, @PathVariable(VERSION) @Parameter(description = "Release version (from maven metadata.xml)", example = "10.0.20", - in = ParameterIn.PATH) String version) throws URISyntaxException { - String externalDocumentURI = externalDocumentService.findExternalDocumentURI(id, version); - if (StringUtils.isBlank(externalDocumentURI)) { + in = ParameterIn.PATH) String version) { + ExternalDocumentMeta externalDocument = externalDocumentService.findExternalDocument(id, version); + if (externalDocument == null) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } - return new ResponseEntity<>(new URI(externalDocumentURI), HttpStatus.OK); + + var model = ExternalDocumentModel.builder().productId(externalDocument.getProductId()) + .version(externalDocument.getVersion()).relativeLink(externalDocument.getRelativeLink()) + .artifactName(externalDocument.getArtifactName()).build(); + model.add(linkTo(methodOn(ExternalDocumentController.class).findExternalDocument(id, version)).withSelfRel()); + + return new ResponseEntity<>(model, HttpStatus.OK); } @PutMapping(SYNC) @@ -59,7 +66,8 @@ public ResponseEntity syncDocumentForProduct( @RequestHeader(value = AUTHORIZATION) String authorizationHeader, @RequestParam(value = RESET_SYNC, required = false) Boolean resetSync) { String token = AuthorizationUtils.getBearerToken(authorizationHeader); - gitHubService.validateUserOrganization(token, GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME); + gitHubService.validateUserInOrganizationAndTeam(token, GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME, + GitHubConstants.AXONIVY_MARKET_TEAM_NAME); var message = new Message(); List products = externalDocumentService.findAllProductsHaveDocument(); if (ObjectUtils.isEmpty(products)) { 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 998fc254b..5afd3de45 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 @@ -1,7 +1,6 @@ package com.axonivy.market.controller; import com.axonivy.market.assembler.FeedbackModelAssembler; -import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.entity.Feedback; import com.axonivy.market.model.FeedbackModel; import com.axonivy.market.model.FeedbackModelRequest; 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 d40565bd5..25e360c0e 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 @@ -4,6 +4,7 @@ import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.entity.Product; import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; import com.axonivy.market.github.service.GitHubService; import com.axonivy.market.model.Message; import com.axonivy.market.model.ProductCustomSortRequest; @@ -19,6 +20,7 @@ import jakarta.validation.Valid; import lombok.AllArgsConstructor; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.StopWatch; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; @@ -28,10 +30,20 @@ import org.springframework.hateoas.PagedModel; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; import java.util.List; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + import static com.axonivy.market.constants.RequestMappingConstants.*; import static com.axonivy.market.constants.RequestParamConstants.*; import static org.springframework.http.HttpHeaders.AUTHORIZATION; @@ -46,6 +58,7 @@ public class ProductController { private final ProductModelAssembler assembler; private final PagedResourcesAssembler pagedResourcesAssembler; private final MetadataService metadataService; + private final GHAxonIvyMarketRepoService axonIvyMarketRepoService; @GetMapping() @Operation(summary = "Retrieve a paginated list of all products, optionally filtered by type, keyword, and language", @@ -85,7 +98,8 @@ public ResponseEntity> findProducts( public ResponseEntity syncProducts(@RequestHeader(value = AUTHORIZATION) String authorizationHeader, @RequestParam(value = RESET_SYNC, required = false) Boolean resetSync) { String token = AuthorizationUtils.getBearerToken(authorizationHeader); - gitHubService.validateUserOrganization(token, GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME); + gitHubService.validateUserInOrganizationAndTeam(token, GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME, + GitHubConstants.AXONIVY_MARKET_TEAM_NAME); if (Boolean.TRUE.equals(resetSync)) { productService.clearAllProducts(); } @@ -108,9 +122,14 @@ public ResponseEntity syncProducts(@RequestHeader(value = AUTHORIZATION @PutMapping(SYNC_PRODUCT_VERSION) @Operation(hidden = true) - public ResponseEntity syncProductVersions(@RequestHeader(value = AUTHORIZATION) String authorizationHeader) { + public ResponseEntity syncProductVersions(@RequestHeader(value = AUTHORIZATION) String authorizationHeader + ,@RequestParam(value = RESET_SYNC, required = false) Boolean resetSync) { String token = AuthorizationUtils.getBearerToken(authorizationHeader); - gitHubService.validateUserOrganization(token, GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME); + gitHubService.validateUserInOrganizationAndTeam(token, GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME, + GitHubConstants.AXONIVY_MARKET_TEAM_NAME); + if (Boolean.TRUE.equals(resetSync)) { + productService.clearAllProductVersion(); + } int nonSyncResult = metadataService.syncAllProductsMetadata(); var message = new Message(); HttpStatus statusCode = HttpStatus.OK; @@ -125,13 +144,47 @@ public ResponseEntity syncProductVersions(@RequestHeader(value = AUTHOR return new ResponseEntity<>(message, statusCode); } + @PutMapping(SYNC_ONE_PRODUCT_BY_ID) + @Operation(hidden = true) + public ResponseEntity syncOneProduct( + @RequestHeader(value = AUTHORIZATION) String authorizationHeader, + @PathVariable(ID) @Parameter(description = "Product Id is defined in meta.json file", example = "a-trust", + in = ParameterIn.PATH) String productId, + @RequestParam(value = MARKET_ITEM_PATH) @Parameter( + description = "Item folder path of the market in https://github.com/axonivy-market/market", + example = "market/connector/a-trust") String marketItemPath, + @RequestParam(value = OVERRIDE_MARKET_ITEM_PATH, required = false) Boolean overrideMarketItemPath) { + String token = AuthorizationUtils.getBearerToken(authorizationHeader); + gitHubService.validateUserInOrganizationAndTeam(token, GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME, + GitHubConstants.AXONIVY_MARKET_TEAM_NAME); + + var message = new Message(); + if (StringUtils.isNotBlank(marketItemPath) && Boolean.TRUE.equals( + overrideMarketItemPath) && CollectionUtils.isEmpty( + axonIvyMarketRepoService.getMarketItemByPath(marketItemPath))) { + message.setHelpCode(ErrorCode.PRODUCT_NOT_FOUND.getCode()); + message.setMessageDetails(ErrorCode.PRODUCT_NOT_FOUND.getHelpText()); + return new ResponseEntity<>(message, HttpStatus.OK); + } + + var isSuccess = productService.syncOneProduct(productId, marketItemPath, overrideMarketItemPath); + if (isSuccess) { + message.setHelpCode(ErrorCode.SUCCESSFUL.getCode()); + message.setMessageDetails("Sync successfully!"); + } else { + message.setMessageDetails("Sync unsuccessfully!"); + } + return new ResponseEntity<>(message, HttpStatus.OK); + } + @PostMapping(CUSTOM_SORT) @Operation(hidden = true) public ResponseEntity createCustomSortProducts( @RequestHeader(value = AUTHORIZATION) String authorizationHeader, @RequestBody @Valid ProductCustomSortRequest productCustomSortRequest) { String token = AuthorizationUtils.getBearerToken(authorizationHeader); - gitHubService.validateUserOrganization(token, GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME); + gitHubService.validateUserInOrganizationAndTeam(token, GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME, + GitHubConstants.AXONIVY_MARKET_TEAM_NAME); productService.addCustomSortProduct(productCustomSortRequest); var message = new Message(ErrorCode.SUCCESSFUL.getCode(), ErrorCode.SUCCESSFUL.getHelpText(), "Custom product sort order added successfully"); diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java index 76c6dedd3..da2608706 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java @@ -10,7 +10,9 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.tags.Tag; +import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -127,4 +129,17 @@ public ResponseEntity> findVersionsForDesigner(@PathVar List versionList = versionService.getVersionsForDesigner(id); return new ResponseEntity<>(versionList, HttpStatus.OK); } + + @GetMapping(LATEST_ARTIFACT_DOWNLOAD_URL_BY_ID) + @Operation(summary = "Get the download url of latest version from artifact by its id and target version", + description = "Return the download url of artifact from version and id") + public ResponseEntity getLatestArtifactDownloadUrl( + @PathVariable(value = ID) @Parameter(in = ParameterIn.PATH, example = "demos-app") String productId, + @RequestParam(value = VERSION) @Parameter(in = ParameterIn.QUERY, example = "10.0-dev") String version, + @RequestParam(value = ARTIFACT) @Parameter(in = ParameterIn.QUERY, + example = "ivy-demos-app.zip") String artifactId) { + String downloadUrl = versionService.getLatestVersionArtifactDownloadUrl(productId, version, artifactId); + HttpStatusCode statusCode = StringUtils.isBlank(downloadUrl) ? HttpStatus.NOT_FOUND : HttpStatus.OK; + return new ResponseEntity<>(downloadUrl, statusCode); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/ExternalDocumentMeta.java b/marketplace-service/src/main/java/com/axonivy/market/entity/ExternalDocumentMeta.java index a52393a28..2fb652d6e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/ExternalDocumentMeta.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/ExternalDocumentMeta.java @@ -1,6 +1,7 @@ package com.axonivy.market.entity; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -17,11 +18,14 @@ @Setter @AllArgsConstructor @NoArgsConstructor +@Builder @Document(EXTERNAL_DOCUMENT_META) public class ExternalDocumentMeta { @Id private String id; private String productId; + private String artifactId; + private String artifactName; private String version; private String storageDirectory; private String relativeLink; diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java index c4ecedfa6..00bb7907c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java @@ -46,6 +46,4 @@ public Map> getAdditionalArtifactsByVersion() { } return this.additionalArtifactsByVersion; } - - -} \ No newline at end of file +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java index 690a9c481..7b998e643 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java @@ -45,6 +45,12 @@ public class Product implements Serializable { private List tags; private String vendor; private String vendorUrl; + @Transient + private String vendorImagePath; + @Transient + private String vendorImageDarkModePath; + private String vendorImage; + private String vendorImageDarkMode; private String platformReview; private String cost; private String repositoryName; 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 89e5a5389..0bdb8fcdf 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 @@ -15,8 +15,7 @@ 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"), USER_NOT_FOUND("2103", "USER_NOT_FOUND"), + GH_FILE_STATUS_INVALID("0201", "GIT_HUB_FILE_STATUS_INVALID"), USER_NOT_FOUND("2103", "USER_NOT_FOUND"), GITHUB_USER_NOT_FOUND("2204", "GITHUB_USER_NOT_FOUND"), GITHUB_USER_UNAUTHORIZED("2205", "GITHUB_USER_UNAUTHORIZED"), FEEDBACK_NOT_FOUND("3103", "FEEDBACK_NOT_FOUND"), NO_FEEDBACK_OF_USER_FOR_PRODUCT("3103", "NO_FEEDBACK_OF_USER_FOR_PRODUCT"), ARGUMENT_BAD_REQUEST("4000", "ARGUMENT_BAD_REQUEST"), 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 500448c8e..536cf92d4 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,6 +1,5 @@ package com.axonivy.market.enums; -import com.axonivy.market.exceptions.model.NotFoundException; import lombok.AllArgsConstructor; import lombok.Getter; import org.apache.commons.lang3.StringUtils; @@ -8,7 +7,7 @@ @Getter @AllArgsConstructor public enum FileType { - META("meta.json"), LOGO("logo.png"); + META("meta.json"), LOGO("logo.png"), OTHER("other"); private final String fileName; @@ -18,6 +17,6 @@ public static FileType of(String name) { return type; } } - throw new NotFoundException(ErrorCode.GH_FILE_TYPE_INVALID, "FileType: " + name); + return OTHER; } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java b/marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java index 68b1f72cf..fa2c08c15 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java @@ -5,6 +5,7 @@ import org.apache.commons.lang3.StringUtils; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @@ -66,4 +67,9 @@ public static String findById(String id, String currentPath) { String nonStandardPath = findById(id).pathToProductFolder; return StringUtils.isNotBlank(nonStandardPath) ? nonStandardPath : currentPath; } + + public static boolean isMsGraphProduct(String productId) { + return List.of(MICROSOFT_REPO_NAME.id, MICROSOFT_365.id, + MICROSOFT_CALENDAR.id, MICROSOFT_MAIL.id, MICROSOFT_TEAMS.id, MICROSOFT_TODO.id).contains(productId); + } } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java b/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java index b4f6dc31f..55e36e917 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java +++ b/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java @@ -62,6 +62,8 @@ public static Product mappingByMetaJSONFile(Product product, GHContent ghContent product.setShortDescriptions(mappingMultilingualismValueByMetaJSONFile(meta.getDescriptions())); product.setVendor(StringUtils.defaultIfEmpty(meta.getVendor(), MetaConstants.DEFAULT_VENDOR_NAME)); product.setVendorUrl(StringUtils.defaultIfEmpty(meta.getVendorUrl(), MetaConstants.DEFAULT_VENDOR_URL)); + product.setVendorImagePath(meta.getVendorImage()); + product.setVendorImageDarkModePath(meta.getVendorImageDarkMode()); product.setPlatformReview(meta.getPlatformReview()); product.setStatusBadgeUrl(meta.getStatusBadgeUrl()); product.setLanguage(meta.getLanguage()); @@ -77,10 +79,12 @@ public static Product mappingByMetaJSONFile(Product product, GHContent ghContent artifact -> artifact.setInvalidArtifact(!artifact.getArtifactId().contains(meta.getId()))); product.setArtifacts(artifacts); product.setReleasedVersions(new ArrayList<>()); + return product; } public static void transferComputedPersistedDataToProduct(Product persisted, Product product) { + product.setMarketDirectory(persisted.getMarketDirectory()); product.setCustomOrder(persisted.getCustomOrder()); product.setNewestReleaseVersion(persisted.getNewestReleaseVersion()); product.setReleasedVersions(persisted.getReleasedVersions()); diff --git a/marketplace-service/src/main/java/com/axonivy/market/factory/VersionFactory.java b/marketplace-service/src/main/java/com/axonivy/market/factory/VersionFactory.java index 03edf3c56..c27aedc8b 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/factory/VersionFactory.java +++ b/marketplace-service/src/main/java/com/axonivy/market/factory/VersionFactory.java @@ -1,9 +1,13 @@ package com.axonivy.market.factory; +import com.axonivy.market.comparator.LatestVersionComparator; import com.axonivy.market.comparator.MavenVersionComparator; +import com.axonivy.market.entity.Metadata; import com.axonivy.market.enums.DevelopmentVersion; +import com.axonivy.market.util.VersionUtils; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.List; @@ -12,7 +16,6 @@ import static com.axonivy.market.constants.MavenConstants.DEV_RELEASE_POSTFIX; import static org.apache.commons.lang3.StringUtils.EMPTY; - @NoArgsConstructor(access = AccessLevel.PRIVATE) public class VersionFactory { @@ -33,8 +36,42 @@ public static String get(List versions, String requestedVersion) { return findVersionStartWith(sortedVersions, requestedVersion); } + public static String getFromMetadata(List metadataList, String requestedVersion) { + var version = DevelopmentVersion.of(requestedVersion); + + // Get latest dev version from metadata + if (Objects.nonNull(version) && version != DevelopmentVersion.LATEST) { + return metadataList.stream().map(Metadata::getLatest).min(new LatestVersionComparator()).orElse(EMPTY); + } + + List artifactVersions = metadataList.stream().flatMap(metadata -> metadata.getVersions().stream()).sorted( + new LatestVersionComparator()).toList(); + List releasedVersions = artifactVersions.stream().filter(VersionUtils::isReleasedVersion).sorted( + new LatestVersionComparator()).toList(); + + // Get latest released version from metadata + if (version == DevelopmentVersion.LATEST) { + return releasedVersions.stream().min(new LatestVersionComparator()).orElse(EMPTY); + } + + // Get latest dev version from specific version + if (requestedVersion.endsWith(DEV_RELEASE_POSTFIX)) { + requestedVersion = requestedVersion.replace(DEV_RELEASE_POSTFIX, EMPTY); + return findVersionStartWith(artifactVersions, requestedVersion); + } + + String matchVersion = findVersionStartWith(releasedVersions, requestedVersion); + + // Return latest version of specific version if can not fnd latest release of that version + if ((VersionUtils.isMajorVersion(requestedVersion) || VersionUtils.isMinorVersion( + requestedVersion)) && !CollectionUtils.containsInstance(releasedVersions, matchVersion)) { + return findVersionStartWith(artifactVersions, requestedVersion); + } + return matchVersion; + } + private static String findVersionStartWith(List releaseVersions, String version) { - return Optional.ofNullable(releaseVersions).orElse(List.of()).stream().filter( + return CollectionUtils.isEmpty(releaseVersions) ? version : releaseVersions.stream().filter( ver -> ver.startsWith(version)).findAny().orElse(version); } } 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 89171a989..f2034ea6f 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 @@ -31,6 +31,8 @@ public class Meta { private Boolean listed; private String version; private String vendor; + private String vendorImage; + private String vendorImageDarkMode; private String vendorUrl; private List tags; private List mavenArtifacts; 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 a75787bd1..45523a09b 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 @@ -17,4 +17,6 @@ public interface GHAxonIvyMarketRepoService { List fetchMarketItemsBySHA1Range(String fromSHA1, String toSHA1); GHRepository getRepository(); + + List getMarketItemByPath(String itemPath); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java index b4b8821c1..1ad461751 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.util.List; +import java.util.Map; public interface GHAxonIvyProductRepoService { @@ -19,4 +20,7 @@ public interface GHAxonIvyProductRepoService { void extractReadMeFileFromContents(Product product, List contents, ProductModuleContent productModuleContent); + + void updateSetupPartForProductModuleContent(Product product, + Map> moduleContents, String tag) throws IOException; } 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 a31309e4a..dca4173d9 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 @@ -19,6 +19,8 @@ public interface GitHubService { GitHub getGitHub() throws IOException; + GitHub getGitHub(String accessToken) throws IOException; + GHOrganization getOrganization(String orgName) throws IOException; GHRepository getRepository(String repositoryPath) throws IOException; @@ -34,5 +36,5 @@ GitHubAccessTokenResponse getAccessToken(String code, GitHubProperty gitHubPrope User getAndUpdateUser(String accessToken); - void validateUserOrganization(String accessToken, String organization) throws UnauthorizedException; + void validateUserInOrganizationAndTeam(String accessToken, String team, String org) throws UnauthorizedException; } 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 e59833d5e..8b64bd8cb 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 @@ -99,7 +99,7 @@ public List fetchMarketItemsBySHA1Range(String fromSHA1, String toSH } GitHubUtils.mapPagedIteratorToList(listFiles).forEach(file -> { String fullPathName = file.getFileName(); - if (FileType.of(fullPathName) != null) { + if (FileType.of(fullPathName) != FileType.OTHER) { var gitHubFile = new GitHubFile(); gitHubFile.setFileName(fullPathName); gitHubFile.setPath(file.getRawUrl().getPath()); @@ -135,4 +135,15 @@ public GHRepository getRepository() { return repository; } + @Override + public List getMarketItemByPath(String itemPath) { + List ghContent = new ArrayList<>(); + try { + ghContent = gitHubService.getDirectoryContent(getRepository(), + itemPath, marketRepoBranch); + } catch (Exception e) { + log.error("Cannot fetch GHContent: ", e); + } + return ghContent; + } } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java index 45d681b8e..69853795a 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java @@ -8,6 +8,7 @@ import com.axonivy.market.constants.ReadmeConstants; import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.enums.Language; import com.axonivy.market.enums.NonStandardProduct; import com.axonivy.market.github.service.GHAxonIvyProductRepoService; import com.axonivy.market.github.service.GitHubService; @@ -18,6 +19,7 @@ import com.axonivy.market.util.VersionUtils; import lombok.extern.log4j.Log4j2; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ObjectUtils; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; @@ -37,11 +39,14 @@ import java.util.Optional; import static com.axonivy.market.constants.CommonConstants.IMAGE_ID_PREFIX; +import static com.axonivy.market.constants.GitHubConstants.MG_GRAPH_IMAGES_FOR_SETUP_FILE; +import static com.axonivy.market.constants.GitHubConstants.MS_GRAPH_PRODUCT_DIRECTORY; +import static com.axonivy.market.constants.ReadmeConstants.SETUP_FILE; +import static com.axonivy.market.util.ProductContentUtils.SETUP; @Log4j2 @Service public class GHAxonIvyProductRepoServiceImpl implements GHAxonIvyProductRepoService { - public static final String IMAGE_EXTENSION = "(.*?).(jpeg|jpg|png|gif)"; private final GitHubService gitHubService; private final ImageService imageService; private GHOrganization organization; @@ -56,7 +61,9 @@ public GHAxonIvyProductRepoServiceImpl(GitHubService gitHubService, ImageService private static GHContent getProductJsonFile(List contents) { return contents.stream().filter(GHContent::isFile) - .filter(content -> ProductJsonConstants.PRODUCT_JSON_FILE.equals(content.getName())).findFirst().orElse(null); + .filter(content -> ProductJsonConstants.PRODUCT_JSON_FILE.equals(content.getName())) + .findFirst() + .orElse(null); } @Override @@ -84,10 +91,10 @@ public List getAllTagsFromRepoName(String repoName) throws IOException { @Override public ProductModuleContent getReadmeAndProductContentsFromTag(Product product, GHRepository ghRepository, String tag) { - ProductModuleContent productModuleContent = ProductContentUtils.initProductModuleContent(product, tag, + ProductModuleContent productModuleContent = ProductContentUtils.initProductModuleContent(product.getId(), tag, new HashSet<>()); try { - List contents = getProductFolderContents(product, ghRepository, tag); + List contents = getProductFolderContents(product.getId(), ghRepository, tag); updateDependencyContentsFromProductJson(productModuleContent, contents, product); extractReadMeFileFromContents(product, contents, productModuleContent); } catch (Exception e) { @@ -108,9 +115,11 @@ public void extractReadMeFileFromContents(Product product, List conte for (GHContent readmeFile : readmeFiles) { String readmeContents = new String(readmeFile.read().readAllBytes()); if (ProductContentUtils.hasImageDirectives(readmeContents)) { - readmeContents = updateImagesWithDownloadUrl(product, contents, readmeContents); + readmeContents = updateImagesWithDownloadUrl(product.getId(), contents, readmeContents); } ProductContentUtils.getExtractedPartsOfReadme(moduleContents, readmeContents, readmeFile.getName()); + updateSetupPartForProductModuleContent(product, moduleContents, + productModuleContent.getTag()); } ProductContentUtils.updateProductModuleTabContents(productModuleContent, moduleContents); } @@ -119,6 +128,36 @@ public void extractReadMeFileFromContents(Product product, List conte } } + @Override + public void updateSetupPartForProductModuleContent(Product product, + Map> moduleContents, String tag) throws IOException { + if (!NonStandardProduct.isMsGraphProduct(product.getId())) { + return; + } + + GHRepository ghRepository = gitHubService.getRepository(product.getRepositoryName()); + List contents = ghRepository.getDirectoryContent(MS_GRAPH_PRODUCT_DIRECTORY, tag); + + GHContent setupFile = contents.stream().filter(GHContent::isFile) + .filter(content -> content.getName().equalsIgnoreCase(SETUP_FILE)) + .findFirst().orElse(null); + + if (ObjectUtils.isNotEmpty(setupFile)) { + String setupContent = new String(setupFile.read().readAllBytes()); + if (ProductContentUtils.hasImageDirectives(setupContent)) { + List setupImagesFolder = + contents.stream().filter(content -> content.getName().equals(MG_GRAPH_IMAGES_FOR_SETUP_FILE)).toList(); + setupContent = updateImagesWithDownloadUrl(product.getId(), setupImagesFolder, setupContent); + } + + if (setupContent.contains(ReadmeConstants.SETUP_PART)) { + List extractSetupContent = List.of(setupContent.split(ReadmeConstants.SETUP_PART)); + setupContent = ProductContentUtils.removeFirstLine(extractSetupContent.get(1)); + } + ProductContentUtils.addLocaleContent(moduleContents, SETUP, setupContent, Language.EN.getValue()); + } + } + private void updateDependencyContentsFromProductJson(ProductModuleContent productModuleContent, List contents, Product product) throws IOException { GHContent productJsonFile = getProductJsonFile(contents); @@ -142,11 +181,11 @@ public String extractProductJsonContent(GHContent ghContent, String tag) { } } - public String updateImagesWithDownloadUrl(Product product, List contents, String readmeContents) { + public String updateImagesWithDownloadUrl(String productId, List contents, String readmeContents) { List allContentOfImages = getAllImagesFromProductFolder(contents); Map imageUrls = new HashMap<>(); - allContentOfImages.forEach(content -> Optional.of(imageService.mappingImageFromGHContent(product, content, false)) + allContentOfImages.forEach(content -> Optional.of(imageService.mappingImageFromGHContent(productId, content, false)) .ifPresent(image -> imageUrls.put(content.getName(), IMAGE_ID_PREFIX.concat(image.getId())))); return ProductContentUtils.replaceImageDirWithImageCustomId(imageUrls, readmeContents); } @@ -157,12 +196,12 @@ private List getAllImagesFromProductFolder(List productFol return images; } - private List getProductFolderContents(Product product, GHRepository ghRepository, String tag) + private List getProductFolderContents(String productId, GHRepository ghRepository, String tag) throws IOException { String productFolderPath = ghRepository.getDirectoryContent(CommonConstants.SLASH, tag).stream() .filter(GHContent::isDirectory).map(GHContent::getName) .filter(content -> content.endsWith(MavenConstants.PRODUCT_ARTIFACT_POSTFIX)).findFirst().orElse(null); - productFolderPath = NonStandardProduct.findById(product.getId(), productFolderPath); + productFolderPath = NonStandardProduct.findById(productId, productFolderPath); return ghRepository.getDirectoryContent(productFolderPath, tag); } 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 4ff0bc3e8..293850c62 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,6 +1,5 @@ package com.axonivy.market.github.service.impl; -import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.ErrorMessageConstants; import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.entity.User; @@ -12,13 +11,13 @@ import com.axonivy.market.github.model.GitHubAccessTokenResponse; import com.axonivy.market.github.model.GitHubProperty; import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.github.util.GitHubUtils; import com.axonivy.market.repository.UserRepository; import lombok.extern.log4j.Log4j2; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHTag; +import org.kohsuke.github.GHTeam; import org.kohsuke.github.GitHub; import org.kohsuke.github.GitHubBuilder; import org.springframework.boot.web.client.RestTemplateBuilder; @@ -30,9 +29,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import java.io.IOException; @@ -64,6 +63,11 @@ public GitHub getGitHub() throws IOException { Optional.ofNullable(gitHubProperty).map(GitHubProperty::getToken).orElse(EMPTY).trim()).build(); } + @Override + public GitHub getGitHub(String accessToken) throws IOException { + return new GitHubBuilder().withOAuthToken(accessToken).build(); + } + @Override public GHOrganization getOrganization(String orgName) throws IOException { return getGitHub().getOrganization(orgName); @@ -156,31 +160,40 @@ public User getAndUpdateUser(String accessToken) { } @Override - public void validateUserOrganization(String accessToken, String organization) throws UnauthorizedException { - List> userOrganizations = getUserOrganizations(accessToken); - for (var org : userOrganizations) { - if (org.get("login").equals(organization)) { + public void validateUserInOrganizationAndTeam(String accessToken, String organization, + String team) throws UnauthorizedException { + try { + var gitHub = getGitHub(accessToken); + if (isUserInOrganizationAndTeam(gitHub, organization, team)) { return; } + } catch (IOException e) { + log.error(e.getStackTrace()); } + throw new UnauthorizedException(ErrorCode.GITHUB_USER_UNAUTHORIZED.getCode(), - ErrorCode.GITHUB_USER_UNAUTHORIZED.getHelpText() - + "-User must be a member of the Axon Ivy Market Organization"); + String.format(ErrorMessageConstants.INVALID_USER_ERROR, ErrorCode.GITHUB_USER_UNAUTHORIZED.getHelpText(), + team, organization)); } - public List> getUserOrganizations(String accessToken) throws UnauthorizedException { - HttpHeaders headers = new HttpHeaders(); - headers.setBearerAuth(accessToken); - HttpEntity entity = new HttpEntity<>(headers); - try { - ResponseEntity>> response = restTemplate.exchange(GitHubConstants.Url.USER_ORGS, - HttpMethod.GET, entity, new ParameterizedTypeReference<>() { - }); - return response.getBody(); - } catch (HttpClientErrorException exception) { - throw new UnauthorizedException(ErrorCode.GITHUB_USER_UNAUTHORIZED.getCode(), - ErrorCode.GITHUB_USER_UNAUTHORIZED.getHelpText() + CommonConstants.DASH_SEPARATOR - + GitHubUtils.extractMessageFromExceptionMessage(exception.getMessage())); + private boolean isUserInOrganizationAndTeam(GitHub gitHub, String organization, + String teamName) throws IOException { + if (gitHub == null) { + return false; + } + + var hashMapTeams = gitHub.getMyTeams(); + var hashSetTeam = hashMapTeams.get(organization); + if (CollectionUtils.isEmpty(hashSetTeam)) { + return false; } + + for (GHTeam team: hashSetTeam) { + if (teamName.equals(team.getName())) { + return true; + } + } + + return false; } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ExternalDocumentModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/ExternalDocumentModel.java new file mode 100644 index 000000000..a7ff07d39 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ExternalDocumentModel.java @@ -0,0 +1,43 @@ +package com.axonivy.market.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +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; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ExternalDocumentModel extends RepresentationModel { + @Schema(description = "Product id", example = "portal") + private String productId; + @Schema(description = "Name of artifact", example = "Portal Guide") + private String artifactName; + @Schema(description = "Version of artifact", example = "10.0.0") + private String version; + @Schema(description = "Relative link of document page", + example = "/market-cache/portal/portal-guide/10.0.12/doc/index.html") + private String relativeLink; + + @Override + public int hashCode() { + return new HashCodeBuilder().append(productId).append(version).hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + var compareExternalDocumentModel = (ExternalDocumentModel) obj; + return new EqualsBuilder().append(productId, compareExternalDocumentModel.getProductId()) + .append(version, compareExternalDocumentModel.getVersion()).isEquals(); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactModel.java index 3ee59d055..1bb392b42 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactModel.java @@ -29,4 +29,6 @@ public class MavenArtifactModel implements Serializable { private String downloadUrl; @JsonIgnore private boolean isInvalidArtifact; -} \ No newline at end of file + @JsonIgnore + private String artifactId; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java index 5e55e4574..74ece6e18 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java @@ -14,6 +14,13 @@ public class ProductDetailModel extends ProductModel { @Schema(description = "Product vendor", example = "Axon Ivy AG") private String vendor; + @Schema(description = "Product vendor url", example = "https://www.axonivy.com") + private String vendorUrl; + @Schema(description = "Product vendor image", example = "https://api.example.com/api/image/67079ca57b9ee74b16c18111") + private String vendorImage; + @Schema(description = "Product vendor image dark mode", + example = "https://api.example.com/api/image/67079ca57b9ee74b16c18111") + private String vendorImageDarkMode; @Schema(description = "Platform review", example = "4.5") private String platformReview; @Schema(description = "Latest release version from maven", example = "v10.0.25") 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 a531ac080..34e51620e 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 @@ -31,8 +31,7 @@ public class ProductModel extends RepresentationModel { "from the Axon Ivy platform\" }") private Map shortDescriptions; @Schema(description = "Product's logo url", - example = "https://raw.githubusercontent.com/axonivy-market/market/feature/MARP-463-Multilingualism-for-Website" + - "/market/connector/jira/logo.png") + example = "https://api.example.com/api/image/67079ca57b9ee74b16c18111") private String logoUrl; @Schema(description = "Type of product", example = "connector") private String type; diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/CustomProductRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/CustomProductRepository.java index 93d39e5dd..fbedca61c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/CustomProductRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/CustomProductRepository.java @@ -13,8 +13,6 @@ public interface CustomProductRepository { Product getProductById(String id); - Product getProductByIdWithNewestReleaseVersion(String id, Boolean isShowDevVersion); - List getReleasedVersionsById(String id); int updateInitialCount(String productId, int initialCount); diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ExternalDocumentMetaRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ExternalDocumentMetaRepository.java index 8b6e5c4c4..0c6ee535f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ExternalDocumentMetaRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ExternalDocumentMetaRepository.java @@ -12,4 +12,6 @@ public interface ExternalDocumentMetaRepository extends MongoRepository findByProductIdAndVersion(String productId, String version); List findByProductId(String productId); + + void deleteByProductIdAndVersion(String productId, String version); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/MetadataRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/MetadataRepository.java index ebd264657..e409d4681 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/MetadataRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/MetadataRepository.java @@ -7,4 +7,8 @@ public interface MetadataRepository extends MongoRepository { List findByProductId(String productId); + + List findByProductIdAndArtifactId(String productId, String artifactId); + + void deleteAllByProductId(String productId); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/MetadataSyncRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/MetadataSyncRepository.java index b4e7317aa..e124f4ef1 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/MetadataSyncRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/MetadataSyncRepository.java @@ -4,4 +4,5 @@ import org.springframework.data.mongodb.repository.MongoRepository; public interface MetadataSyncRepository extends MongoRepository { + void deleteAllByProductId(String productId); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductJsonContentRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductJsonContentRepository.java index 7b616da5f..428d1b631 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductJsonContentRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductJsonContentRepository.java @@ -10,4 +10,6 @@ public interface ProductJsonContentRepository extends MongoRepository { List findByProductIdAndVersion(String productId, String version); + + void deleteAllByProductId(String productId); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductModuleContentRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductModuleContentRepository.java index 1436e6d90..760f0407e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductModuleContentRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductModuleContentRepository.java @@ -8,4 +8,6 @@ public interface ProductModuleContentRepository extends MongoRepository, CustomProductModuleContentRepository { ProductModuleContent findByTagAndProductId(String tag, String productId); + + void deleteAllByProductId(String productId); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java b/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java index 7328de2ab..672ba7323 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java @@ -12,10 +12,8 @@ import com.axonivy.market.repository.CustomProductRepository; import com.axonivy.market.repository.CustomRepository; import com.axonivy.market.repository.ProductModuleContentRepository; -import com.axonivy.market.util.VersionUtils; import lombok.AllArgsConstructor; import lombok.Builder; -import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.bson.BsonRegularExpression; import org.springframework.data.domain.Page; @@ -42,7 +40,7 @@ public class CustomProductRepositoryImpl extends CustomRepository implements Cus public static final String LOCALIZE_SEARCH_PATTERN = "%s.%s"; final MongoTemplate mongoTemplate; - final ProductModuleContentRepository contentRepository; + final ProductModuleContentRepository contentRepo; public Product queryProductByAggregation(Aggregation aggregation) { return Optional.of(mongoTemplate.aggregate(aggregation, EntityConstants.PRODUCT, Product.class)) @@ -64,19 +62,6 @@ public Product getProductByIdWithTagOrVersion(String id, String tag) { return result; } - @Override - public Product getProductByIdWithNewestReleaseVersion(String id, Boolean isShowDevVersion) { - Product result = findProductById(id); - if (ObjectUtils.isEmpty(result)) { - return null; - } - List devVersions = VersionUtils.getVersionsToDisplay(result.getReleasedVersions(), isShowDevVersion, null); - String currentTag = VersionUtils.convertVersionToTag(result.getId(), devVersions.get(0)); - ProductModuleContent content = contentRepository.findByTagAndProductId(currentTag, id); - result.setProductModuleContent(content); - return result; - } - @Override public ProductModuleContent findByProductIdAndTagOrMavenVersion(String productId, String tag) { Criteria productIdCriteria = Criteria.where(MongoDBConstants.PRODUCT_ID).is(productId); @@ -97,7 +82,7 @@ private Product findProductById(String id) { public Product getProductById(String id) { Product result = findProductById(id); if (!Objects.isNull(result)) { - ProductModuleContent content = contentRepository.findByTagAndProductId( + ProductModuleContent content = contentRepo.findByTagAndProductId( result.getNewestReleaseVersion(), id); result.setProductModuleContent(content); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/ExternalDocumentService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ExternalDocumentService.java index e2b5139f0..2a09fddbe 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/ExternalDocumentService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/ExternalDocumentService.java @@ -1,5 +1,6 @@ package com.axonivy.market.service; +import com.axonivy.market.entity.ExternalDocumentMeta; import com.axonivy.market.entity.Product; import java.util.List; @@ -9,5 +10,5 @@ public interface ExternalDocumentService { List findAllProductsHaveDocument(); - String findExternalDocumentURI(String productId, String version); + ExternalDocumentMeta findExternalDocument(String productId, String version); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/ImageService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ImageService.java index c30809751..90c8eee1b 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/ImageService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/ImageService.java @@ -1,7 +1,6 @@ package com.axonivy.market.service; import com.axonivy.market.entity.Image; -import com.axonivy.market.entity.Product; import org.bson.types.Binary; import org.kohsuke.github.GHContent; @@ -10,9 +9,9 @@ public interface ImageService { Binary getImageBinary(GHContent ghContent); - Image mappingImageFromGHContent(Product product, GHContent ghContent, boolean isLogo); + Image mappingImageFromGHContent(String productId, GHContent ghContent, boolean isLogo); - Image mappingImageFromDownloadedFolder(Product product, Path imagePath); + Image mappingImageFromDownloadedFolder(String productId, Path imagePath); byte[] readImage(String id); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/MetadataService.java b/marketplace-service/src/main/java/com/axonivy/market/service/MetadataService.java index b0e134240..bbb275f2e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/MetadataService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/MetadataService.java @@ -1,5 +1,9 @@ package com.axonivy.market.service; +import com.axonivy.market.entity.Product; + public interface MetadataService { + int syncAllProductsMetadata(); + boolean syncProductMetadata(Product product); } 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 73ae5de55..a1e6368bb 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 @@ -27,4 +27,7 @@ public interface ProductService { Product fetchProductDetailByIdAndVersion(String id, String version); + boolean syncOneProduct(String productId, String marketItemPath, Boolean overrideMarketItemPath); + + void clearAllProductVersion(); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java b/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java index 3e913fed0..7449004a5 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java @@ -15,4 +15,5 @@ List getArtifactsAndVersionToDisplay(String productId List getVersionsForDesigner(String productId); + String getLatestVersionArtifactDownloadUrl(String productId, String version, String artifact); } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ExternalDocumentServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ExternalDocumentServiceImpl.java index 4b15c2438..799a647b9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ExternalDocumentServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ExternalDocumentServiceImpl.java @@ -1,6 +1,7 @@ package com.axonivy.market.service.impl; import com.axonivy.market.bo.Artifact; +import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.DirectoryConstants; import com.axonivy.market.entity.ExternalDocumentMeta; import com.axonivy.market.entity.Product; @@ -15,6 +16,7 @@ import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.RegExUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; @@ -29,6 +31,7 @@ public class ExternalDocumentServiceImpl implements ExternalDocumentService { private static final String DOC_URL_PATTERN = "/%s/index.html"; + private static final String MS_WIN_SEPARATOR = "\\\\"; final ProductRepository productRepo; final ExternalDocumentMetaRepository externalDocumentMetaRepo; final FileDownloadService fileDownloadService; @@ -56,37 +59,43 @@ public List findAllProductsHaveDocument() { } @Override - public String findExternalDocumentURI(String productId, String version) { + public ExternalDocumentMeta findExternalDocument(String productId, String version) { var product = productRepo.findById(productId); if (product.isEmpty()) { - return EMPTY; + return null; } List docMetas = externalDocumentMetaRepo.findByProductId(productId); List docMetaVersion = docMetas.stream().map(ExternalDocumentMeta::getVersion).toList(); String resolvedVersion = VersionFactory.get(docMetaVersion, version); return docMetas.stream().filter(meta -> StringUtils.equals(meta.getVersion(), resolvedVersion)) - .map(ExternalDocumentMeta::getRelativeLink).findAny().orElse(EMPTY); + .findAny().orElse(null); } private void syncDocumentationForProduct(String productId, boolean isResetSync, Artifact artifact, List releasedVersions) { for (var version : releasedVersions) { - List documentMetas = externalDocumentMetaRepo.findByProductIdAndVersion(productId, version); - if (!isResetSync && ObjectUtils.isNotEmpty(documentMetas)) { - continue; + if (isResetSync) { + externalDocumentMetaRepo.deleteByProductIdAndVersion(productId, version); + } else { + if (ObjectUtils.isNotEmpty(externalDocumentMetaRepo.findByProductIdAndVersion(productId, version))) { + continue; + } } String downloadDocUrl = MavenUtils.buildDownloadUrl(artifact, version); String location = downloadDocAndUnzipToShareFolder(downloadDocUrl, isResetSync); if (StringUtils.isNoneBlank(location)) { - // Remove all old records - externalDocumentMetaRepo.deleteAll(documentMetas); var documentMeta = new ExternalDocumentMeta(); documentMeta.setProductId(productId); + documentMeta.setArtifactId(artifact.getArtifactId()); + documentMeta.setArtifactName(artifact.getName()); documentMeta.setVersion(version); documentMeta.setStorageDirectory(location); + // remove prefix 'data' and replace all ms win separator to slash if present var locationRelative = location.substring(location.indexOf(DirectoryConstants.CACHE_DIR)); - documentMeta.setRelativeLink(String.format(DOC_URL_PATTERN, locationRelative)); + locationRelative = RegExUtils.replaceAll(String.format(DOC_URL_PATTERN, locationRelative), MS_WIN_SEPARATOR, + CommonConstants.SLASH); + documentMeta.setRelativeLink(locationRelative); externalDocumentMetaRepo.save(documentMeta); } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ImageServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ImageServiceImpl.java index 03c273937..ab700efa0 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ImageServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ImageServiceImpl.java @@ -1,11 +1,12 @@ package com.axonivy.market.service.impl; import com.axonivy.market.entity.Image; -import com.axonivy.market.entity.Product; import com.axonivy.market.github.util.GitHubUtils; import com.axonivy.market.repository.ImageRepository; +import com.axonivy.market.service.FileDownloadService; import com.axonivy.market.service.ImageService; import com.axonivy.market.util.MavenUtils; +import lombok.AllArgsConstructor; import lombok.extern.log4j.Log4j2; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.ObjectUtils; @@ -22,13 +23,11 @@ @Service @Log4j2 +@AllArgsConstructor public class ImageServiceImpl implements ImageService { private final ImageRepository imageRepository; - - public ImageServiceImpl(ImageRepository imageRepository) { - this.imageRepository = imageRepository; - } + private final FileDownloadService fileDownloadService; @Override public Binary getImageBinary(GHContent ghContent) { @@ -37,36 +36,50 @@ public Binary getImageBinary(GHContent ghContent) { byte[] sourceBytes = IOUtils.toByteArray(contentStream); return new Binary(sourceBytes); } catch (Exception exception) { - log.error("Cannot get content of product logo {} ", ghContent.getName()); + log.error("Cannot get content of product image {} ", ghContent.getName()); + return null; + } + } + + private Binary getImageByDownloadUrl(String downloadUrl) { + try { + byte[] downloadedImage = fileDownloadService.downloadFile(downloadUrl); + return new Binary(downloadedImage); + } catch (Exception exception) { + log.error("Cannot download the image from the url: {} with error {}", downloadUrl, exception.getMessage()); return null; } } @Override - public Image mappingImageFromGHContent(Product product, GHContent ghContent, boolean isLogo) { + public Image mappingImageFromGHContent(String productId, GHContent ghContent, boolean isLogo) { if (ObjectUtils.isEmpty(ghContent)) { - log.info("There is missing for image content for product {}", product.getId()); + log.info("There is missing for image content for product {}", productId); return null; } if (!isLogo) { - Image existsImage = imageRepository.findByProductIdAndSha(product.getId(), ghContent.getSha()); + Image existsImage = imageRepository.findByProductIdAndSha(productId, ghContent.getSha()); if (ObjectUtils.isNotEmpty(existsImage)) { return existsImage; } } - Image image = new Image(); + String currentImageUrl = GitHubUtils.getDownloadUrl(ghContent); - image.setProductId(product.getId()); + Binary imageContent = Optional.ofNullable(getImageBinary(ghContent)) + .orElseGet(() -> getImageByDownloadUrl(currentImageUrl)); + + Image image = new Image(); + image.setProductId(productId); image.setImageUrl(currentImageUrl); - image.setImageData(getImageBinary(ghContent)); + image.setImageData(imageContent); image.setSha(ghContent.getSha()); return imageRepository.save(image); } @Override - public Image mappingImageFromDownloadedFolder(Product product, Path imagePath) { - List existingImages = imageRepository.findByProductId(product.getId()); + public Image mappingImageFromDownloadedFolder(String productId, Path imagePath) { + List existingImages = imageRepository.findByProductId(productId); try { InputStream contentStream = MavenUtils.extractedContentStream(imagePath); byte[] sourceBytes = IOUtils.toByteArray(contentStream); @@ -79,7 +92,7 @@ public Image mappingImageFromDownloadedFolder(Product product, Path imagePath) { if (ObjectUtils.isEmpty(existedImage)) { Image image = new Image(); image.setImageData(new Binary(sourceBytes)); - image.setProductId(product.getId()); + image.setProductId(productId); return imageRepository.save(image); } return existedImage; diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/MetadataServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/MetadataServiceImpl.java index 0d6d53dee..9b7508531 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/MetadataServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/MetadataServiceImpl.java @@ -85,48 +85,59 @@ public int syncAllProductsMetadata() { log.warn("**MetadataService: Start to sync version for {} product(s)", products.size()); int nonUpdatedSyncCount = 0; for (Product product : products) { - // Set up cache before sync - String productId = product.getId(); - Set metadataSet = new HashSet<>(metadataRepo.findByProductId(product.getId())); - MavenArtifactVersion artifactVersionCache = mavenArtifactVersionRepo.findById(product.getId()).orElse( - MavenArtifactVersion.builder().productId(productId).build()); - MetadataSync syncCache = metadataSyncRepo.findById(product.getId()).orElse( - MetadataSync.builder().productId(product.getId()).syncedVersions(new HashSet<>()).build()); - Set artifactsFromNewTags = new HashSet<>(); - - // Find artifacts from unhandled tags - List nonSyncedVersionOfTags = VersionUtils.removeSyncedVersionsFromReleasedVersions( - product.getReleasedVersions(), syncCache.getSyncedVersions()); - if (ObjectUtils.isNotEmpty(nonSyncedVersionOfTags)) { - artifactsFromNewTags.addAll(getArtifactsFromNonSyncedVersion(product.getId(), nonSyncedVersionOfTags)); - syncCache.getSyncedVersions().addAll(nonSyncedVersionOfTags); - log.info("**MetadataService: New tags detected: {} in product {}", nonSyncedVersionOfTags.toString(), - productId); - } - - // Sync versions from maven & update artifacts-version table - metadataSet.addAll(MavenUtils.convertArtifactsToMetadataSet(artifactsFromNewTags, productId)); - if (ObjectUtils.isNotEmpty(product.getArtifacts())) { - metadataSet.addAll( - MavenUtils.convertArtifactsToMetadataSet(new HashSet<>(product.getArtifacts()), productId)); - } - if (CollectionUtils.isEmpty(metadataSet)) { - log.info("**MetadataService: No artifact found in product {}", productId); + if (!syncProductMetadata(product)) { nonUpdatedSyncCount += 1; - continue; } - artifactVersionCache.setAdditionalArtifactsByVersion(new HashMap<>()); - updateMavenArtifactVersionData(productId, product.getReleasedVersions(), metadataSet, artifactVersionCache); - - // Persist changed - metadataSyncRepo.save(syncCache); - mavenArtifactVersionRepo.save(artifactVersionCache); - metadataRepo.saveAll(metadataSet); } log.warn("**MetadataService: version sync finished"); return nonUpdatedSyncCount; } + @Override + public boolean syncProductMetadata(Product product) { + if (product == null) { + return false; + } + + // Set up cache before sync + String productId = product.getId(); + Set metadataSet = new HashSet<>(metadataRepo.findByProductId(product.getId())); + MavenArtifactVersion artifactVersionCache = mavenArtifactVersionRepo.findById(product.getId()).orElse( + MavenArtifactVersion.builder().productId(productId).build()); + MetadataSync syncCache = metadataSyncRepo.findById(product.getId()).orElse( + MetadataSync.builder().productId(product.getId()).syncedVersions(new HashSet<>()).build()); + Set artifactsFromNewTags = new HashSet<>(); + + // Find artifacts from unhandled tags + List nonSyncedVersionOfTags = VersionUtils.removeSyncedVersionsFromReleasedVersions( + product.getReleasedVersions(), syncCache.getSyncedVersions()); + if (ObjectUtils.isNotEmpty(nonSyncedVersionOfTags)) { + artifactsFromNewTags.addAll(getArtifactsFromNonSyncedVersion(product.getId(), nonSyncedVersionOfTags)); + syncCache.getSyncedVersions().addAll(nonSyncedVersionOfTags); + log.info("**MetadataService: New tags detected: {} in product {}", nonSyncedVersionOfTags.toString(), + productId); + } + + // Sync versions from maven & update artifacts-version table + metadataSet.addAll(MavenUtils.convertArtifactsToMetadataSet(artifactsFromNewTags, productId)); + if (ObjectUtils.isNotEmpty(product.getArtifacts())) { + metadataSet.addAll( + MavenUtils.convertArtifactsToMetadataSet(new HashSet<>(product.getArtifacts()), productId)); + } + if (CollectionUtils.isEmpty(metadataSet)) { + log.info("**MetadataService: No artifact found in product {}", productId); + return false; + } + artifactVersionCache.setAdditionalArtifactsByVersion(new HashMap<>()); + updateMavenArtifactVersionData(productId, product.getReleasedVersions(), metadataSet, artifactVersionCache); + + // Persist changed + metadataSyncRepo.save(syncCache); + mavenArtifactVersionRepo.save(artifactVersionCache); + metadataRepo.saveAll(metadataSet); + return true; + } + public void updateContentsFromNonMatchVersions(String productId, List releasedVersions, Metadata metadata) { List productModuleContents = new ArrayList<>(); @@ -223,13 +234,13 @@ private void updateProductJsonAndReadmeContents(String productId, String metaVer private ProductModuleContent getReadmeAndProductContentsFromTag(Product product, String nonMatchSnapshotVersion, Metadata snapShotMetadata, String url) { - ProductModuleContent productModuleContent = ProductContentUtils.initProductModuleContent(product, Strings.EMPTY, + ProductModuleContent productModuleContent = ProductContentUtils.initProductModuleContent(product.getId(), Strings.EMPTY, Set.of(nonMatchSnapshotVersion)); String unzippedFolderPath = Strings.EMPTY; try { unzippedFolderPath = fileDownloadService.downloadAndUnzipProductContentFile(url, snapShotMetadata); updateDependencyContentsFromProductJson(productModuleContent, product, unzippedFolderPath); - extractReadMeFileFromContents(product, unzippedFolderPath, productModuleContent); + extractReadMeFileFromContents(product.getId(), unzippedFolderPath, productModuleContent); } catch (Exception e) { log.error("Cannot get product.json content in {}", e.getMessage()); return null; @@ -253,7 +264,7 @@ private void updateDependencyContentsFromProductJson(ProductModuleContent produc ProductJsonConstants.VERSION_VALUE, product); } - private void extractReadMeFileFromContents(Product product, String unzippedFolderPath, + private void extractReadMeFileFromContents(String productId, String unzippedFolderPath, ProductModuleContent productModuleContent) { try { List readmeFiles; @@ -266,7 +277,7 @@ private void extractReadMeFileFromContents(Product product, String unzippedFolde for (Path readmeFile : readmeFiles) { String readmeContents = Files.readString(readmeFile); if (ProductContentUtils.hasImageDirectives(readmeContents)) { - readmeContents = updateImagesWithDownloadUrl(product, unzippedFolderPath, readmeContents); + readmeContents = updateImagesWithDownloadUrl(productId, unzippedFolderPath, readmeContents); } ProductContentUtils.getExtractedPartsOfReadme(moduleContents, readmeContents, readmeFile.getFileName().toString()); @@ -278,7 +289,7 @@ private void extractReadMeFileFromContents(Product product, String unzippedFolde } } - private String updateImagesWithDownloadUrl(Product product, String unzippedFolderPath, + private String updateImagesWithDownloadUrl(String productId, String unzippedFolderPath, String readmeContents) throws IOException { List allImagePaths; Map imageUrls = new HashMap<>(); @@ -287,7 +298,7 @@ private String updateImagesWithDownloadUrl(Product product, String unzippedFolde path -> path.getFileName().toString().toLowerCase().matches(CommonConstants.IMAGE_EXTENSION)).toList(); } allImagePaths.forEach( - imagePath -> Optional.of(imageService.mappingImageFromDownloadedFolder(product, imagePath)).ifPresent( + imagePath -> Optional.of(imageService.mappingImageFromDownloadedFolder(productId, imagePath)).ifPresent( image -> imageUrls.put(imagePath.getFileName().toString(), CommonConstants.IMAGE_ID_PREFIX.concat(image.getId())))); @@ -342,4 +353,4 @@ public Set getArtifactsFromNonSyncedVersion(String productId, List filterNonPersistGhTagName(List currentTags) { @@ -143,7 +156,7 @@ public Page findProducts(String type, String keyword, String language, if (BooleanUtils.isTrue(isRESTClient)) { searchCriteria.setExcludeFields(List.of(SHORT_DESCRIPTIONS)); } - return productRepository.searchByCriteria(searchCriteria, searchPageable); + return productRepo.searchByCriteria(searchCriteria, searchPageable); } @Override @@ -165,25 +178,24 @@ public List syncLatestDataFromMarketRepo() { @Override public int updateInstallationCountForProduct(String key, String designerVersion) { - Product product = productRepository.getProductById(key); + Product product = productRepo.getProductById(key); if (Objects.isNull(product)) { return 0; } log.info("Increase installation count for product {} By Designer Version {}", key, designerVersion); if (StringUtils.isNotBlank(designerVersion)) { - productRepository.increaseInstallationCountForProductByDesignerVersion(key, designerVersion); + productRepo.increaseInstallationCountForProductByDesignerVersion(key, designerVersion); } log.info("updating installation count for product {}", key); if (BooleanUtils.isTrue(product.getSynchronizedInstallationCount())) { - return productRepository.increaseInstallationCount(key); + return productRepo.increaseInstallationCount(key); } syncInstallationCountWithProduct(product); - return productRepository.updateInitialCount(key, product.getInstallationCount() + 1); + return productRepo.updateInitialCount(key, product.getInstallationCount() + 1); } - public void syncInstallationCountWithProduct(Product product) { log.info("synchronizing installation count for product {}", product.getId()); try { @@ -212,7 +224,7 @@ private void syncRepoMetaDataStatus() { marketRepoMeta.setRepoName(GitHubConstants.AXONIVY_MARKETPLACE_REPO_NAME); marketRepoMeta.setLastSHA1(lastGHCommit.getSHA1()); marketRepoMeta.setLastChange(GitHubUtils.getGHCommitDate(lastGHCommit)); - gitHubRepoMetaRepository.save(marketRepoMeta); + gitHubRepoMetaRepo.save(marketRepoMeta); marketRepoMeta = null; } @@ -225,7 +237,7 @@ private List updateLatestChangeToProductsFromGithubRepo() { Map> groupGitHubFiles = new HashMap<>(); for (var file : gitHubFileChanges) { String filePath = file.getFileName(); - var parentPath = filePath.replace(FileType.META.getFileName(), EMPTY).replace(FileType.LOGO.getFileName(), EMPTY); + var parentPath = filePath.substring(0, filePath.lastIndexOf(CommonConstants.SLASH) + 1); var files = groupGitHubFiles.getOrDefault(parentPath, new ArrayList<>()); files.add(file); files.sort((file1, file2) -> GitHubUtils.sortMetaJsonFirst(file1.getFileName(), file2.getFileName())); @@ -252,19 +264,19 @@ private String removeProductAndImage(GitHubFile file) { String[] splitMetaJsonPath = file.getFileName().split(SLASH); String extractMarketDirectory = file.getFileName().replace(splitMetaJsonPath[splitMetaJsonPath.length - 1], EMPTY); - List productList = productRepository.findByMarketDirectory(extractMarketDirectory); + List productList = productRepo.findByMarketDirectory(extractMarketDirectory); if (ObjectUtils.isNotEmpty(productList)) { productId = productList.get(0).getId(); - productRepository.deleteById(productId); - imageRepository.deleteAllByProductId(productId); + productRepo.deleteById(productId); + imageRepo.deleteAllByProductId(productId); } } else { - List images = imageRepository.findByImageUrlEndsWithIgnoreCase(file.getFileName()); + List images = imageRepo.findByImageUrlEndsWithIgnoreCase(file.getFileName()); if (ObjectUtils.isNotEmpty(images)) { Image currentImage = images.get(0); productId = currentImage.getProductId(); - productRepository.deleteById(productId); - imageRepository.deleteAllByProductId(productId); + productRepo.deleteById(productId); + imageRepo.deleteAllByProductId(productId); } } return productId; @@ -283,11 +295,12 @@ private String modifyProductMetaOrLogo(GitHubFile file, String parentPath) { private String updateProductByMetaJsonAndLogo(GHContent fileContent, GitHubFile file, String parentPath) { String productId; - Product product = new Product(); - ProductFactory.mappingByGHContent(product, fileContent); if (FileType.META == file.getType()) { + Product product = new Product(); + ProductFactory.mappingByGHContent(product, fileContent); + mappingVendorImageFromGHContent(product, fileContent); transferComputedDataFromDB(product); - productId = productRepository.save(product).getId(); + productId = productRepo.save(product).getId(); } else { productId = modifyProductLogo(parentPath, fileContent); } @@ -298,14 +311,14 @@ private String modifyProductLogo(String parentPath, GHContent fileContent) { var searchCriteria = new ProductSearchCriteria(); searchCriteria.setKeyword(parentPath); searchCriteria.setFields(List.of(MARKET_DIRECTORY)); - Product result = productRepository.findByCriteria(searchCriteria); + Product result = productRepo.findByCriteria(searchCriteria); if (result != null) { - Optional.ofNullable(imageService.mappingImageFromGHContent(result, fileContent, true)).ifPresent(image -> { + Optional.ofNullable(imageService.mappingImageFromGHContent(result.getId(), fileContent, true)).ifPresent(image -> { if (StringUtils.isNotBlank(result.getLogoId())) { - imageRepository.deleteById(result.getLogoId()); + imageRepo.deleteById(result.getLogoId()); } result.setLogoId(image.getId()); - productRepository.save(result); + productRepo.save(result); }); return result.getId(); } @@ -337,7 +350,7 @@ public Order createOrder(SortOption sortOption, String language) { } private Order getExtensionOrder(String language) { - List customSorts = productCustomSortRepository.findAll(); + List customSorts = productCustomSortRepo.findAll(); if (!customSorts.isEmpty()) { SortOption sortOptionExtension = SortOption.of(customSorts.get(0).getRuleForRemainder()); @@ -349,7 +362,7 @@ private Order getExtensionOrder(String language) { private boolean isLastGithubCommitCovered() { boolean isLastCommitCovered = false; long lastCommitTime = 0L; - marketRepoMeta = gitHubRepoMetaRepository.findByRepoName(GitHubConstants.AXONIVY_MARKETPLACE_REPO_NAME); + marketRepoMeta = gitHubRepoMetaRepo.findByRepoName(GitHubConstants.AXONIVY_MARKETPLACE_REPO_NAME); if (marketRepoMeta != null) { lastCommitTime = marketRepoMeta.getLastChange(); } @@ -362,7 +375,7 @@ private boolean isLastGithubCommitCovered() { } private void updateLatestReleaseTagContentsFromProductRepo() { - List products = productRepository.findAll(); + List products = productRepo.findAll(); if (ObjectUtils.isEmpty(products)) { return; } @@ -370,27 +383,15 @@ private void updateLatestReleaseTagContentsFromProductRepo() { for (Product product : products) { if (StringUtils.isNotBlank(product.getRepositoryName())) { getProductContents(product); - productRepository.save(product); + productRepo.save(product); } } } - private void updateProductContentForNonStandardProduct(Map.Entry> ghContentEntity, - Product product) { - ProductModuleContent initialContent = new ProductModuleContent(); - initialContent.setTag(INITIAL_VERSION); - initialContent.setProductId(product.getId()); - ProductFactory.mappingIdForProductModuleContent(initialContent); - product.setReleasedVersions(List.of(INITIAL_VERSION)); - product.setNewestReleaseVersion(INITIAL_VERSION); - axonIvyProductRepoService.extractReadMeFileFromContents(product, ghContentEntity.getValue(), initialContent); - productModuleContentRepository.save(initialContent); - } - private void getProductContents(Product product) { try { - GHRepository productRepo = gitHubService.getRepository(product.getRepositoryName()); - updateProductFromReleaseTags(product, productRepo); + GHRepository productRepository = gitHubService.getRepository(product.getRepositoryName()); + updateProductFromReleaseTags(product, productRepository); } catch (IOException e) { log.error("Cannot find product repository {} {}", product.getRepositoryName(), e); } @@ -404,32 +405,54 @@ private List syncProductsFromGitHubRepo() { var product = new Product(); //update the meta.json first ghContentEntity.getValue().sort((f1, f2) -> GitHubUtils.sortMetaJsonFirst(f1.getName(), f2.getName())); + for (var content : ghContentEntity.getValue()) { ProductFactory.mappingByGHContent(product, content); + mappingVendorImageFromGHContent(product, content); mappingLogoFromGHContent(product, content); } - if (productRepository.findById(product.getId()).isPresent()) { + if (productRepo.findById(product.getId()).isPresent()) { continue; } - if (StringUtils.isNotBlank(product.getRepositoryName())) { - updateProductCompatibility(product); - getProductContents(product); - } else { - updateProductContentForNonStandardProduct(ghContentEntity, product); - } + updateRelatedThingsOfProductFromGHContent(ghContentEntity.getValue(), product); transferComputedDataFromDB(product); - syncedProductIds.add(productRepository.save(product).getId()); + syncedProductIds.add(productRepo.save(product).getId()); } return syncedProductIds; } private void mappingLogoFromGHContent(Product product, GHContent ghContent) { - if (StringUtils.endsWith(ghContent.getName(), LOGO_FILE)) { - Optional.ofNullable(imageService.mappingImageFromGHContent(product, ghContent, true)) + if (ghContent != null && StringUtils.endsWith(ghContent.getName(), LOGO_FILE)) { + Optional.ofNullable(imageService.mappingImageFromGHContent(product.getId(), ghContent, true)) .ifPresent(image -> product.setLogoId(image.getId())); } } + private void mappingVendorImageFromGHContent(Product product, GHContent ghContent) { + if (StringUtils.endsWith(ghContent.getName(), MetaConstants.META_FILE)) { + if (StringUtils.isNotBlank(product.getVendorImagePath())) { + product.setVendorImage(mapVendorImage(product.getId(), ghContent, product.getVendorImagePath())); + } + if (StringUtils.isNotBlank(product.getVendorImageDarkModePath())) { + product.setVendorImageDarkMode(mapVendorImage(product.getId(), ghContent, product.getVendorImageDarkModePath())); + } + } + } + + private String mapVendorImage(String productId, GHContent ghContent, String imageName) { + if (StringUtils.isNotBlank(imageName)) { + String imagePath = StringUtils.replace(ghContent.getPath(), MetaConstants.META_FILE, imageName); + try { + GHContent imageContent = gitHubService.getGHContent(ghContent.getOwner(), imagePath, marketRepoBranch); + return Optional.ofNullable(imageService.mappingImageFromGHContent(productId, imageContent, false)) + .map(Image::getId).orElse(EMPTY); + } catch (IOException e) { + log.error("Get Vendor Image failed: ", e); + } + } + return EMPTY; + } + private void updateProductFromReleaseTags(Product product, GHRepository productRepo) { List productModuleContents = new ArrayList<>(); List ghTags = getProductReleaseTags(product); @@ -441,7 +464,7 @@ private void updateProductFromReleaseTags(Product product, GHRepository productR product.setNewestReleaseVersion(lastTag.getName()); List currentTags = VersionUtils.getReleaseTagsFromProduct(product); if (CollectionUtils.isEmpty(currentTags)) { - currentTags = productModuleContentRepository.findTagsByProductId(product.getId()); + currentTags = productModuleContentRepo.findTagsByProductId(product.getId()); } ghTags = ghTags.stream().filter(filterNonPersistGhTagName(currentTags)).toList(); @@ -458,7 +481,7 @@ private void updateProductFromReleaseTags(Product product, GHRepository productR product.getReleasedVersions().add(versionFromTag); } if (!CollectionUtils.isEmpty(productModuleContents)) { - productModuleContentRepository.saveAll(productModuleContents); + productModuleContentRepo.saveAll(productModuleContents); } } @@ -510,7 +533,7 @@ public String getCompatibilityFromOldestTag(String oldestTag) { @Override public Product fetchProductDetail(String id, Boolean isShowDevVersion) { - Product product = productRepository.getProductByIdWithNewestReleaseVersion(id, isShowDevVersion); + Product product = getProductByIdWithNewestReleaseVersion(id, isShowDevVersion); return Optional.ofNullable(product).map(productItem -> { updateProductInstallationCount(id, productItem); return productItem; @@ -519,14 +542,13 @@ public Product fetchProductDetail(String id, Boolean isShowDevVersion) { @Override public Product fetchBestMatchProductDetail(String id, String version) { - MavenArtifactVersion existingMavenArtifactVersion = mavenArtifactVersionRepo.findById(id).orElse( - MavenArtifactVersion.builder().productId(id).build()); - List versions = MavenUtils.getAllExistingVersions(existingMavenArtifactVersion, true, - null); - String bestMatchVersion = VersionUtils.getBestMatchVersion(versions, version); + List installableVersions = VersionUtils.getInstallableVersionsFromMetadataList( + metadataRepo.findByProductId(id)); + String bestMatchVersion = VersionUtils.getBestMatchVersion(installableVersions, version); String bestMatchTag = VersionUtils.convertVersionToTag(id, bestMatchVersion); - Product product = StringUtils.isBlank(bestMatchTag) ? productRepository.getProductByIdWithNewestReleaseVersion( - id, false) : productRepository.getProductByIdWithTagOrVersion(id, bestMatchTag); + // Cover exception case of employee onboarding without any product.json file + Product product = StringUtils.isBlank(bestMatchTag) ? getProductByIdWithNewestReleaseVersion(id, + false) : productRepo.getProductByIdWithTagOrVersion(id, bestMatchTag); return Optional.ofNullable(product).map(productItem -> { updateProductInstallationCount(id, productItem); productItem.setBestMatchVersion(bestMatchVersion); @@ -534,23 +556,41 @@ public Product fetchBestMatchProductDetail(String id, String version) { }).orElse(null); } + public Product getProductByIdWithNewestReleaseVersion(String id, Boolean isShowDevVersion) { + List versions; + String version = StringUtils.EMPTY; + var mavenArtifactVersion = mavenArtifactVersionRepo.findById(id); + if(mavenArtifactVersion.isPresent()) { + versions = MavenUtils.getAllExistingVersions(mavenArtifactVersion.get(), BooleanUtils.isTrue(isShowDevVersion), + StringUtils.EMPTY); + version = VersionUtils.convertVersionToTag(id, CollectionUtils.firstElement(versions)); + } + // Cover exception case of employee onboarding without any product.json file + if (StringUtils.isBlank(version)) { + versions = VersionUtils.getVersionsToDisplay(productRepo.getReleasedVersionsById(id), isShowDevVersion, + StringUtils.EMPTY); + version = CollectionUtils.firstElement(versions); + } + return productRepo.getProductByIdWithTagOrVersion(id, version); + } + public void updateProductInstallationCount(String id, Product productItem) { if (!BooleanUtils.isTrue(productItem.getSynchronizedInstallationCount())) { syncInstallationCountWithProduct(productItem); - int persistedInitialCount = productRepository.updateInitialCount(id, productItem.getInstallationCount()); + int persistedInitialCount = productRepo.updateInitialCount(id, productItem.getInstallationCount()); productItem.setInstallationCount(persistedInitialCount); } } @Override public Product fetchProductDetailByIdAndVersion(String id, String version) { - return productRepository.getProductByIdWithTagOrVersion(id, version); + return productRepo.getProductByIdWithTagOrVersion(id, version); } @Override public void clearAllProducts() { - gitHubRepoMetaRepository.deleteAll(); - productRepository.deleteAll(); + gitHubRepoMetaRepo.deleteAll(); + productRepo.deleteAll(); } @Override @@ -558,10 +598,10 @@ public void addCustomSortProduct(ProductCustomSortRequest customSort) throws Inv SortOption.of(customSort.getRuleForRemainder()); ProductCustomSort productCustomSort = new ProductCustomSort(customSort.getRuleForRemainder()); - productCustomSortRepository.deleteAll(); + productCustomSortRepo.deleteAll(); removeFieldFromAllProductDocuments(ProductJsonConstants.CUSTOM_ORDER); - productCustomSortRepository.save(productCustomSort); - productRepository.saveAll(refineOrderedListOfProductsInCustomSort(customSort.getOrderedListOfProducts())); + productCustomSortRepo.save(productCustomSort); + productRepo.saveAll(refineOrderedListOfProductsInCustomSort(customSort.getOrderedListOfProducts())); } public List refineOrderedListOfProductsInCustomSort(List orderedListOfProducts) @@ -570,14 +610,14 @@ public List refineOrderedListOfProductsInCustomSort(List ordere int descendingOrder = orderedListOfProducts.size(); for (String productId : orderedListOfProducts) { - Optional productOptional = productRepository.findById(productId); + Optional productOptional = productRepo.findById(productId); if (productOptional.isEmpty()) { throw new InvalidParamException(ErrorCode.PRODUCT_NOT_FOUND, "Not found product with id: " + productId); } Product product = productOptional.get(); product.setCustomOrder(descendingOrder--); - productRepository.save(product); + productRepo.save(product); productEntries.add(product); } @@ -590,8 +630,91 @@ public void removeFieldFromAllProductDocuments(String fieldName) { } public void transferComputedDataFromDB(Product product) { - productRepository.findById(product.getId()).ifPresent(persistedData -> - ProductFactory.transferComputedPersistedDataToProduct(persistedData, product)); + productRepo.findById(product.getId()).ifPresent(persistedData -> + ProductFactory.transferComputedPersistedDataToProduct(persistedData, product) + ); + } + + @Override + public boolean syncOneProduct(String productId, String marketItemPath, Boolean overrideMarketItemPath) { + try { + log.info("Sync product {} is starting ...", productId); + log.info("Clean up product {}", productId); + Product product = renewProductById(productId, marketItemPath, overrideMarketItemPath); + log.info("Get data of product {} from the git hub", productId); + var gitHubContents = axonIvyMarketRepoService.getMarketItemByPath(product.getMarketDirectory()); + if (!CollectionUtils.isEmpty(gitHubContents)) { + log.info("Update data of product {} from meta.json and logo files", productId); + mappingMetaDataAndLogoFromGHContent(gitHubContents, product); + updateRelatedThingsOfProductFromGHContent(gitHubContents, product); + productRepo.save(product); + metadataService.syncProductMetadata(product); + log.info("Sync product {} is finished!", productId); + return true; + } + } catch (Exception e) { + log.error(e.getStackTrace()); + } + return false; + } + + @Override + public void clearAllProductVersion() { + metadataRepo.deleteAll(); + metadataSyncRepo.deleteAll(); + mavenArtifactVersionRepo.deleteAll(); + } + + private Product renewProductById(String productId, String marketItemPath, Boolean overrideMarketItemPath) { + Product product = new Product(); + productRepo.findById(productId).ifPresent(foundProduct -> { + ProductFactory.transferComputedPersistedDataToProduct(foundProduct, product); + imageRepo.deleteAllByProductId(foundProduct.getId()); + metadataRepo.deleteAllByProductId(foundProduct.getId()); + metadataSyncRepo.deleteAllByProductId(foundProduct.getId()); + mavenArtifactVersionRepo.deleteAllById(List.of(foundProduct.getId())); + productModuleContentRepo.deleteAllByProductId(foundProduct.getId()); + productJsonContentRepo.deleteAllByProductId(foundProduct.getId()); + productRepo.delete(foundProduct); + } + ); + + if (StringUtils.isNotBlank(marketItemPath) && Boolean.TRUE.equals(overrideMarketItemPath)) { + product.setMarketDirectory(marketItemPath); + } + product.setNewestReleaseVersion(EMPTY); + + return product; } + private void mappingMetaDataAndLogoFromGHContent(List gitHubContent, Product product) { + var gitHubContents = new ArrayList<>(gitHubContent); + gitHubContents.sort((f1, f2) -> GitHubUtils.sortMetaJsonFirst(f1.getName(), f2.getName())); + for (var content : gitHubContent) { + ProductFactory.mappingByGHContent(product, content); + mappingVendorImageFromGHContent(product, content); + mappingLogoFromGHContent(product, content); + } + } + + private void updateRelatedThingsOfProductFromGHContent(List gitHubContents, Product product) { + if (StringUtils.isNotBlank(product.getRepositoryName())) { + updateProductCompatibility(product); + getProductContents(product); + } else { + updateProductContentForNonStandardProduct(gitHubContents, product); + } + } + + private void updateProductContentForNonStandardProduct(List ghContentEntity, + Product product) { + ProductModuleContent initialContent = new ProductModuleContent(); + initialContent.setTag(INITIAL_VERSION); + initialContent.setProductId(product.getId()); + ProductFactory.mappingIdForProductModuleContent(initialContent); + product.setReleasedVersions(List.of(INITIAL_VERSION)); + product.setNewestReleaseVersion(INITIAL_VERSION); + axonIvyProductRepoService.extractReadMeFileFromContents(product, ghContentEntity, initialContent); + productModuleContentRepo.save(initialContent); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java index 9313190c8..b6328cc15 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java @@ -1,13 +1,18 @@ package com.axonivy.market.service.impl; import com.axonivy.market.bo.Artifact; +import com.axonivy.market.comparator.LatestVersionComparator; +import com.axonivy.market.constants.MavenConstants; import com.axonivy.market.controller.ProductDetailsController; import com.axonivy.market.entity.MavenArtifactVersion; +import com.axonivy.market.entity.Metadata; import com.axonivy.market.entity.ProductJsonContent; +import com.axonivy.market.factory.VersionFactory; import com.axonivy.market.model.MavenArtifactModel; import com.axonivy.market.model.MavenArtifactVersionModel; import com.axonivy.market.model.VersionAndUrlModel; import com.axonivy.market.repository.MavenArtifactVersionRepository; +import com.axonivy.market.repository.MetadataRepository; import com.axonivy.market.repository.ProductJsonContentRepository; import com.axonivy.market.repository.ProductModuleContentRepository; import com.axonivy.market.repository.ProductRepository; @@ -19,14 +24,18 @@ import lombok.AllArgsConstructor; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.hateoas.Link; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import static com.axonivy.market.constants.ProductJsonConstants.NAME; @@ -38,15 +47,16 @@ @AllArgsConstructor public class VersionServiceImpl implements VersionService { - private final MavenArtifactVersionRepository mavenArtifactVersionRepository; + private final MavenArtifactVersionRepository mavenArtifactVersionRepo; private final ProductRepository productRepo; private final ProductJsonContentRepository productJsonRepo; private final ProductModuleContentRepository productContentRepo; private final ObjectMapper mapper = new ObjectMapper(); + private final MetadataRepository metadataRepo; public List getArtifactsAndVersionToDisplay(String productId, Boolean isShowDevVersion, String designerVersion) { - MavenArtifactVersion existingMavenArtifactVersion = mavenArtifactVersionRepository.findById(productId).orElse( + MavenArtifactVersion existingMavenArtifactVersion = mavenArtifactVersionRepo.findById(productId).orElse( MavenArtifactVersion.builder().productId(productId).build()); List results = new ArrayList<>(); @@ -87,10 +97,14 @@ public Map getProductJsonContentByIdAndTag(String productId, Str @Override public List getVersionsForDesigner(String productId) { List versionAndUrlList = new ArrayList<>(); - MavenArtifactVersion existingMavenArtifactVersion = mavenArtifactVersionRepository.findById(productId).orElse( - MavenArtifactVersion.builder().productId(productId).build()); - List versions = MavenUtils.getAllExistingVersions(existingMavenArtifactVersion, true, - null); + List releasedVersions = + VersionUtils.getInstallableVersionsFromMetadataList(metadataRepo.findByProductId(productId)); + if (CollectionUtils.isEmpty(releasedVersions)) { + return Collections.emptyList(); + } + List versions = releasedVersions.stream().filter( + version -> VersionUtils.isOfficialVersionOrUnReleasedDevVersion(releasedVersions, version)).sorted( + new LatestVersionComparator()).toList(); for (String version : versions) { Link link = linkTo( methodOn(ProductDetailsController.class).findProductJsonContent(productId, version)).withSelfRel(); @@ -119,4 +133,51 @@ public List getMavenArtifactsFromProductJsonByTag(String tag, productJsonRepo.findByProductIdAndVersion(productId, tag).stream().findAny().orElse(null); return MavenUtils.getMavenArtifactsFromProductJson(productJson); } + + public String getLatestVersionArtifactDownloadUrl(String productId, String version, String artifact) { + String[] artifactParts = StringUtils.defaultString(artifact).split(MavenConstants.MAIN_VERSION_REGEX); + if (artifactParts.length < 1) { + return StringUtils.EMPTY; + } + + String artifactId = artifactParts[0]; + String fileType = artifactParts[artifactParts.length - 1]; + List metadataList = metadataRepo.findByProductIdAndArtifactId(productId, artifactId); + if (CollectionUtils.isEmpty(metadataList)) { + return StringUtils.EMPTY; + } + + List modelArtifactIds = metadataList.stream().map(Metadata::getArtifactId).toList(); + String targetVersion = VersionFactory.getFromMetadata(metadataList, version); + if (StringUtils.isBlank(targetVersion)) { + return StringUtils.EMPTY; + } + + var artifactVersionCache = mavenArtifactVersionRepo.findById(productId); + if (artifactVersionCache.isEmpty()) { + return StringUtils.EMPTY; + } + + // Find download url first from product artifact model + String downloadUrl = getDownloadUrlFromExistingDataByArtifactIdAndVersion( + artifactVersionCache.get().getProductArtifactsByVersion(), targetVersion, modelArtifactIds); + // Continue to find download url from artifact in meta.json if it is not existed in artifacts of product.json + if (StringUtils.isBlank(downloadUrl)) { + downloadUrl = getDownloadUrlFromExistingDataByArtifactIdAndVersion( + artifactVersionCache.get().getAdditionalArtifactsByVersion(), targetVersion, modelArtifactIds); + } + + if (!StringUtils.endsWith(downloadUrl, fileType)) { + log.warn("**VersionService: the found downloadUrl {} is not match with file type {}", downloadUrl, fileType); + downloadUrl = StringUtils.EMPTY; + } + return downloadUrl; + } + + public String getDownloadUrlFromExistingDataByArtifactIdAndVersion( + Map> existingData, String version, List artifactsIds) { + return existingData.computeIfAbsent(version, key -> new ArrayList<>()).stream().filter( + model -> artifactsIds.contains(model.getArtifactId())).findAny().map(MavenArtifactModel::getDownloadUrl).orElse( + null); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/MavenUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/MavenUtils.java index f6b6fdd02..95037618f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/util/MavenUtils.java +++ b/marketplace-service/src/main/java/com/axonivy/market/util/MavenUtils.java @@ -31,8 +31,11 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; +import static com.axonivy.market.constants.MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL; + @Log4j2 @NoArgsConstructor(access = AccessLevel.PRIVATE) public class MavenUtils { @@ -121,7 +124,12 @@ public static List extractMavenArtifactsFromContentStream(InputStream // Extract repository URL JsonNode repositoriesNode = dataNode.path(ProductJsonConstants.REPOSITORIES); - String repoUrl = repositoriesNode.get(0).path(ProductJsonConstants.URL).asText(); + String repoUrl = Optional.of(repositoriesNode) + .filter(jsonNode -> !jsonNode.isMissingNode()) + .map(jsonNode -> jsonNode.get(0)) + .map(jsonNode -> jsonNode.get(ProductJsonConstants.URL)) + .map(JsonNode::asText) + .orElse(DEFAULT_IVY_MAVEN_BASE_URL); // Process projects if (dataNode.has(ProductJsonConstants.PROJECTS)) { @@ -139,7 +147,7 @@ public static List extractMavenArtifactsFromContentStream(InputStream public static String buildDownloadUrl(Artifact artifact, String version) { String groupIdByVersion = artifact.getGroupId(); String artifactIdByVersion = artifact.getArtifactId(); - String repoUrl = StringUtils.defaultIfBlank(artifact.getRepoUrl(), MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); + String repoUrl = StringUtils.defaultIfBlank(artifact.getRepoUrl(), DEFAULT_IVY_MAVEN_BASE_URL); ArchivedArtifact archivedArtifactBestMatchVersion = findArchivedArtifactInfoBestMatchWithVersion(version, artifact.getArchivedArtifacts()); @@ -187,7 +195,7 @@ public static MavenArtifactModel convertMavenArtifactToModel(Artifact artifact, } artifact.setType(StringUtils.defaultIfBlank(artifact.getType(), ProductJsonConstants.DEFAULT_PRODUCT_TYPE)); artifactName = String.format(MavenConstants.ARTIFACT_NAME_FORMAT, artifactName, artifact.getType()); - return MavenArtifactModel.builder().name(artifactName).downloadUrl(buildDownloadUrl(artifact, version)).build(); + return MavenArtifactModel.builder().name(artifactName).downloadUrl(buildDownloadUrl(artifact, version)).artifactId(artifact.getArtifactId()).build(); } public static List convertArtifactsToModels(List artifacts, String version) { @@ -206,7 +214,7 @@ public static String buildSnapshotMetadataUrlFromArtifactInfo(String repoUrl, St if (StringUtils.isAnyBlank(groupId, artifactId)) { return StringUtils.EMPTY; } - repoUrl = StringUtils.defaultIfEmpty(repoUrl, MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); + repoUrl = StringUtils.defaultIfEmpty(repoUrl, DEFAULT_IVY_MAVEN_BASE_URL); groupId = groupId.replace(CommonConstants.DOT_SEPARATOR, CommonConstants.SLASH); return String.join(CommonConstants.SLASH, repoUrl, groupId, artifactId, snapshotVersion, MavenConstants.METADATA_URL_POSTFIX); @@ -216,7 +224,7 @@ public static String buildMetadataUrlFromArtifactInfo(String repoUrl, String gro if (StringUtils.isAnyBlank(groupId, artifactId)) { return StringUtils.EMPTY; } - repoUrl = StringUtils.defaultIfEmpty(repoUrl, MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); + repoUrl = StringUtils.defaultIfEmpty(repoUrl, DEFAULT_IVY_MAVEN_BASE_URL); groupId = groupId.replace(CommonConstants.DOT_SEPARATOR, CommonConstants.SLASH); return String.join(CommonConstants.SLASH, repoUrl, groupId, artifactId, MavenConstants.METADATA_URL_POSTFIX); } @@ -232,7 +240,7 @@ public static Metadata convertArtifactToMetadata(String productId, Artifact arti String artifactId = Objects.isNull(archivedArtifact) ? artifact.getArtifactId() : archivedArtifact.getArtifactId(); String groupId = Objects.isNull(archivedArtifact) ? artifact.getGroupId() : archivedArtifact.getGroupId(); String type = StringUtils.defaultIfBlank(artifact.getType(), ProductJsonConstants.DEFAULT_PRODUCT_TYPE); - String repoUrl = StringUtils.defaultIfEmpty(artifact.getRepoUrl(), MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); + String repoUrl = StringUtils.defaultIfEmpty(artifact.getRepoUrl(), DEFAULT_IVY_MAVEN_BASE_URL); artifactName = String.format(MavenConstants.ARTIFACT_NAME_FORMAT, artifactName, type); return Metadata.builder().groupId(groupId).versions(new HashSet<>()).productId(productId).artifactId( @@ -249,10 +257,10 @@ public static Metadata buildSnapShotMetadataFromVersion(Metadata metadata, Strin } public static MavenArtifactModel buildMavenArtifactModelFromMetadata(String version, Metadata metadata) { - return new MavenArtifactModel(metadata.getName(), - buildDownloadUrl(metadata.getArtifactId(), version, metadata.getType(), - metadata.getRepoUrl(), metadata.getGroupId(), metadata.getSnapshotVersionValue()), - metadata.getArtifactId().contains(metadata.getGroupId())); + String downloadUrl = buildDownloadUrl(metadata.getArtifactId(), version, metadata.getType(), metadata.getRepoUrl(), + metadata.getGroupId(), metadata.getSnapshotVersionValue()); + return MavenArtifactModel.builder().name(metadata.getName()).downloadUrl(downloadUrl).isInvalidArtifact( + metadata.getArtifactId().contains(metadata.getGroupId())).artifactId(metadata.getArtifactId()).build(); } public static String getMetadataContentFromUrl(String metadataUrl) { @@ -311,4 +319,9 @@ public static List getAllExistingVersions(MavenArtifactVersion existingM return VersionUtils.getVersionsToDisplay(new ArrayList<>(existingProductsArtifactByVersion), isShowDevVersion, designerVersion); } + + public static boolean isProductMetadata(Metadata metadata) { + return StringUtils.endsWith(Objects.requireNonNullElse(metadata, new Metadata()).getArtifactId(), + MavenConstants.PRODUCT_ARTIFACT_POSTFIX); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/ProductContentUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/ProductContentUtils.java index f8ba2d5e2..bb0fe9f8e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/util/ProductContentUtils.java +++ b/marketplace-service/src/main/java/com/axonivy/market/util/ProductContentUtils.java @@ -4,7 +4,6 @@ import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.constants.ReadmeConstants; -import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductModuleContent; import com.axonivy.market.enums.Language; import com.axonivy.market.factory.ProductFactory; @@ -90,7 +89,7 @@ public static void getExtractedPartsOfReadme(Map> mo addLocaleContent(moduleContents, SETUP, setup.trim(), locale); } - private static void addLocaleContent(Map> moduleContents, String type, String content, + public static void addLocaleContent(Map> moduleContents, String type, String content, String locale) { moduleContents.computeIfAbsent(type, key -> new HashMap<>()).put(locale, content); } @@ -115,9 +114,9 @@ public static String removeFirstLine(String text) { return result; } - public static ProductModuleContent initProductModuleContent(Product product, String tag, Set mavenVersions) { + public static ProductModuleContent initProductModuleContent(String productId, String tag, Set mavenVersions) { ProductModuleContent productModuleContent = new ProductModuleContent(); - productModuleContent.setProductId(product.getId()); + productModuleContent.setProductId(productId); productModuleContent.setTag(tag); productModuleContent.setMavenVersions(mavenVersions); ProductFactory.mappingIdForProductModuleContent(productModuleContent); diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java index 35602c915..ab36f2185 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java +++ b/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java @@ -4,6 +4,7 @@ import com.axonivy.market.comparator.MavenVersionComparator; import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.Metadata; import com.axonivy.market.entity.Product; import com.axonivy.market.enums.NonStandardProduct; import lombok.extern.log4j.Log4j2; @@ -17,10 +18,12 @@ import org.springframework.util.CollectionUtils; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import static com.axonivy.market.constants.MavenConstants.*; @Log4j2 @@ -71,7 +74,7 @@ public static String getBestMatchVersion(List versions, String designerV return bestMatchVersion; } - public static boolean isOfficialVersionOrUnReleasedDevVersion(List versions, String version) { + public static boolean isOfficialVersionOrUnReleasedDevVersion(Collection versions, String version) { if (isReleasedVersion(version)) { return true; } @@ -150,7 +153,7 @@ public static String getOldestVersion(List tags) { if (!CollectionUtils.isEmpty(tags)) { List releasedTags = tags.stream().map(tag -> tag.getName().replaceAll(NON_NUMERIC_CHAR, Strings.EMPTY)) .distinct().sorted(new LatestVersionComparator()).toList(); - return CollectionUtils.lastElement(releasedTags); + result = CollectionUtils.lastElement(releasedTags); } return result; } @@ -170,4 +173,25 @@ public static List removeSyncedVersionsFromReleasedVersions(List } return releasedVersion; } + + public static String getNumbersOnly(String version) { + return StringUtils.defaultIfBlank(version, StringUtils.EMPTY).split(CommonConstants.DASH_SEPARATOR)[0]; + } + + public static boolean isMajorVersion(String version) { + return getNumbersOnly(version).split(MAIN_VERSION_REGEX).length == 1 && isReleasedVersion(version); + } + + public static boolean isMinorVersion(String version) { + return getNumbersOnly(version).split(MAIN_VERSION_REGEX).length == 2 && isReleasedVersion(version); + } + + public static List getInstallableVersionsFromMetadataList(List metadataList) { + if (CollectionUtils.isEmpty(metadataList)) { + return new ArrayList<>(); + } + return metadataList.stream().filter(MavenUtils::isProductMetadata).findAny().map( + metadata -> metadata.getVersions().stream().sorted(new LatestVersionComparator()).collect( + Collectors.toList())).orElse(new ArrayList<>()); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/BaseSetup.java b/marketplace-service/src/test/java/com/axonivy/market/BaseSetup.java index a8cfc76d6..55df55847 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/BaseSetup.java +++ b/marketplace-service/src/test/java/com/axonivy/market/BaseSetup.java @@ -10,15 +10,22 @@ import com.axonivy.market.entity.ProductModuleContent; import com.axonivy.market.enums.Language; import com.axonivy.market.enums.SortOption; +import com.axonivy.market.model.MavenArtifactModel; +import com.axonivy.market.util.MavenUtils; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Assertions; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.util.CollectionUtils; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; @@ -26,14 +33,17 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; @Log4j2 public class BaseSetup { protected static final String SAMPLE_PRODUCT_ID = "amazon-comprehend"; + protected static final String SAMPLE_PRODUCT_PATH = "/market/connector/amazon-comprehend"; protected static final String SAMPLE_PRODUCT_NAME = "prody Comprehend"; protected static final Pageable PAGEABLE = PageRequest.of(0, 20, Sort.by(SortOption.ALPHABETICALLY.getOption()).descending()); protected static final String MOCK_PRODUCT_ID = "bpmn-statistic"; + protected static final String MOCK_PRODUCT_ID_WITH_TAG = "bpmn-statistic-v10.0.10"; protected static final String MOCK_ARTIFACT_ID = "bpmn-statistic"; protected static final String MOCK_PRODUCT_ARTIFACT_ID = "bpmn-statistic-product"; protected static final String MOCK_RELEASED_VERSION = "10.0.10"; @@ -45,11 +55,13 @@ public class BaseSetup { protected static final String MOCK_GROUP_ID = "com.axonivy.util"; protected static final String MOCK_PRODUCT_NAME = "bpmn statistic"; protected static final String MOCK_PRODUCT_JSON_FILE_PATH = "src/test/resources/product.json"; + protected static final String MOCK_PRODUCT_JSON_FILE_PATH_NO_URL = "src/test/resources/productMissingURL.json"; protected static final String MOCK_PRODUCT_JSON_DIR_PATH = "src/test/resources"; protected static final String MOCK_PRODUCT_JSON_NODE_FILE_PATH = "src/test/resources/prouct-json-node.json"; protected static final String MOCK_METADATA_FILE_PATH = "src/test/resources/metadata.xml"; protected static final String MOCK_SNAPSHOT_METADATA_FILE_PATH = "src/test/resources/snapshotMetadata.xml"; - protected static final String INAVALID_FILE_PATH = "test/file/path"; + protected static final String INVALID_FILE_PATH = "test/file/path"; + protected static final String MOCK_SETUP_MD_PATH = "src/test/resources/setup.md"; protected static final String MOCK_MAVEN_URL = "https://maven.axonivy.com/com/axonivy/util/bpmn-statistic/maven" + "-metadata.xml"; protected static final String MOCK_SNAPSHOT_MAVEN_URL = "https://maven.axonivy.com/com/axonivy/util/bpmn-statistic" + @@ -59,6 +71,8 @@ public class BaseSetup { protected static final String MOCK_SNAPSHOT_DOWNLOAD_URL = "https://maven.axonivy" + ".com/com/axonivy/util/bpmn-statistic/10.0.10-SNAPSHOT/bpmn-statistic-10.0.10-SNAPSHOT.zip"; protected static final String MOCK_ARTIFACT_NAME = "bpmn statistic (zip)"; + protected static final String MOCK_ARTIFACT_DOWNLOAD_FILE = "bpmn-statistic.zip"; + protected static final String LEGACY_INSTALLATION_COUNT_PATH_FIELD_NAME = "legacyInstallationCountPath"; protected Page createPageProductsMock() { var mockProducts = new ArrayList(); @@ -104,6 +118,10 @@ protected static ProductJsonContent getMockProductJsonContent() { return result; } + protected static String getMockSetupMd() { + return getContentFromTestResourcePath(MOCK_SETUP_MD_PATH); + } + private static String getContentFromTestResourcePath(String path) { try { return Files.readString(Paths.get(path)); @@ -113,7 +131,7 @@ private static String getContentFromTestResourcePath(String path) { } } - protected String getMockProductJsonNodeContent() { + protected static String getMockProductJsonNodeContent() { return getContentFromTestResourcePath(MOCK_PRODUCT_JSON_NODE_FILE_PATH); } @@ -152,11 +170,23 @@ protected MavenArtifactVersion getMockMavenArtifactVersion() { new HashMap<>()); } + protected MavenArtifactVersion getMockMavenArtifactVersionWithData() { + MavenArtifactVersion mockMavenArtifactVersion = getMockMavenArtifactVersion(); + Map> mockArtifactModelsByVersion = new HashMap<>(); + mockArtifactModelsByVersion.put(MOCK_SNAPSHOT_VERSION, new ArrayList<>()); + mockMavenArtifactVersion.setProductArtifactsByVersion(mockArtifactModelsByVersion); + return mockMavenArtifactVersion; + } + + protected Product getMockProduct() { + Product mockProduct = Product.builder().id(MOCK_PRODUCT_ID).releasedVersions(new ArrayList<>()).artifacts( + List.of(getMockArtifact())).build(); + mockProduct.getReleasedVersions().add(MOCK_RELEASED_VERSION); + return mockProduct; + } + protected List getMockProducts() { - Product mockProduct = - Product.builder().id(MOCK_PRODUCT_ID).releasedVersions(List.of(MOCK_RELEASED_VERSION)).artifacts( - List.of(getMockArtifact())).build(); - return List.of(mockProduct); + return List.of(getMockProduct()); } protected Metadata getMockMetadata() { @@ -164,4 +194,35 @@ protected Metadata getMockMetadata() { MOCK_GROUP_ID).isProductArtifact(true).repoUrl(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL).type( MavenConstants.DEFAULT_PRODUCT_FOLDER_TYPE).name(MOCK_ARTIFACT_NAME).build(); } + + protected static InputStream getMockInputStream() { + String jsonContent = getMockProductJsonContent().getContent(); + return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); + } + + protected static InputStream getMockProductJsonNodeContentInputStream() { + String jsonContent = getContentFromTestResourcePath(MOCK_PRODUCT_JSON_FILE_PATH_NO_URL); + return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); + } + + protected Metadata getMockMetadataWithVersions() { + Metadata mockMetadata = getMockMetadata(); + mockMetadata.setRelease(MOCK_RELEASED_VERSION); + mockMetadata.setLatest(MOCK_SPRINT_RELEASED_VERSION); + mockMetadata.setVersions( + Set.of(MOCK_SNAPSHOT_VERSION, MOCK_RELEASED_VERSION, MOCK_SPRINT_RELEASED_VERSION)); + return mockMetadata; + } + + protected MavenArtifactModel getMockMavenArtifactModel() { + MavenArtifactModel mockMavenArtifactModel = new MavenArtifactModel(); + mockMavenArtifactModel.setName(MOCK_ARTIFACT_NAME); + mockMavenArtifactModel.setDownloadUrl(MOCK_DOWNLOAD_URL); + return mockMavenArtifactModel; + } + + protected MavenArtifactModel getMockMavenArtifactModelWithDownloadUrl() { + return MavenArtifactModel.builder().name(MOCK_PRODUCT_NAME).artifactId(MOCK_ARTIFACT_ID).downloadUrl( + MOCK_DOWNLOAD_URL).build(); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ExternalDocumentControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ExternalDocumentControllerTest.java index 2bf4fe79c..97df45462 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ExternalDocumentControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ExternalDocumentControllerTest.java @@ -1,5 +1,6 @@ package com.axonivy.market.controller; +import com.axonivy.market.entity.ExternalDocumentMeta; import com.axonivy.market.entity.Product; import com.axonivy.market.github.service.GitHubService; import com.axonivy.market.service.ExternalDocumentService; @@ -11,7 +12,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; -import java.net.URISyntaxException; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -36,9 +36,9 @@ class ExternalDocumentControllerTest { @Test - void testFindProductDoc() throws URISyntaxException { - when(service.findExternalDocumentURI(any(), any())).thenReturn("/market-cache/portal/10.0.0/doc/index.html"); - var result = externalDocumentController.findExternalDocumentURI("portal", "10.0"); + void testFindProductDoc() { + when(service.findExternalDocument(any(), any())).thenReturn(createExternalDocumentMock()); + var result = externalDocumentController.findExternalDocument("portal", "10.0"); assertEquals(HttpStatus.OK, result.getStatusCode()); assertTrue(result.hasBody()); assertTrue(ObjectUtils.isNotEmpty(result.getBody())); @@ -54,4 +54,10 @@ void testSyncDocumentForProduct() { result = externalDocumentController.syncDocumentForProduct(TOKEN, true); assertEquals(HttpStatus.OK, result.getStatusCode(), "Should return at least one product"); } + + private ExternalDocumentMeta createExternalDocumentMock() { + return ExternalDocumentMeta.builder() + .relativeLink("/market-cache/portal/10.0.0/doc/index.html") + .build(); + } } 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 63ef385d0..b536e1552 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 @@ -7,14 +7,17 @@ import com.axonivy.market.enums.SortOption; import com.axonivy.market.enums.TypeOption; import com.axonivy.market.exceptions.model.UnauthorizedException; +import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; import com.axonivy.market.github.service.GitHubService; import com.axonivy.market.model.ProductCustomSortRequest; import com.axonivy.market.service.MetadataService; import com.axonivy.market.service.ProductService; +import com.axonivy.market.service.VersionService; import com.axonivy.market.util.AuthorizationUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHContent; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -32,11 +35,12 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ProductControllerTest { + private static final String PRODUCT_ID_SAMPLE = "a-trust"; + private static final String PRODUCT_PATH_SAMPLE = "market/connector/a-trust"; 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 " + @@ -64,6 +68,12 @@ class ProductControllerTest { @Mock private MetadataService metadataService; + @Mock + private VersionService versionService; + + @Mock + private GHAxonIvyMarketRepoService axonIvyMarketRepoService; + @BeforeEach void setup() { assembler = new ProductModelAssembler(); @@ -130,7 +140,7 @@ void testSyncProductsWithResetSuccess() { void testSyncProductsInvalidToken() { doThrow(new UnauthorizedException(ErrorCode.GITHUB_USER_UNAUTHORIZED.getCode(), ErrorCode.GITHUB_USER_UNAUTHORIZED.getHelpText())).when(gitHubService) - .validateUserOrganization(any(String.class), any(String.class)); + .validateUserInOrganizationAndTeam(any(String.class), any(String.class), any(String.class)); UnauthorizedException exception = assertThrows(UnauthorizedException.class, () -> productController.syncProducts(INVALID_AUTHORIZATION_HEADER, false)); @@ -140,12 +150,12 @@ void testSyncProductsInvalidToken() { @Test void testSyncMavenVersionSuccess() { - var response = productController.syncProductVersions(AUTHORIZATION_HEADER); + var response = productController.syncProductVersions(AUTHORIZATION_HEADER, false); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); assertTrue(response.hasBody()); assertEquals(ErrorCode.MAVEN_VERSION_SYNC_FAILED.getCode(), Objects.requireNonNull(response.getBody()).getHelpCode()); when(metadataService.syncAllProductsMetadata()).thenReturn(1); - response = productController.syncProductVersions(AUTHORIZATION_HEADER); + response = productController.syncProductVersions(AUTHORIZATION_HEADER, false); assertEquals(HttpStatus.OK, response.getStatusCode()); assertTrue(response.hasBody()); assertEquals(ErrorCode.SUCCESSFUL.getCode(), Objects.requireNonNull(response.getBody()).getHelpCode()); @@ -156,14 +166,54 @@ void testSyncMavenVersionSuccess() { void testSyncMavenVersionWithInvalidToken() { doThrow(new UnauthorizedException(ErrorCode.GITHUB_USER_UNAUTHORIZED.getCode(), ErrorCode.GITHUB_USER_UNAUTHORIZED.getHelpText())).when(gitHubService) - .validateUserOrganization(any(String.class), any(String.class)); + .validateUserInOrganizationAndTeam(any(String.class), any(String.class), any(String.class)); UnauthorizedException exception = assertThrows(UnauthorizedException.class, - () -> productController.syncProductVersions(INVALID_AUTHORIZATION_HEADER)); + () -> productController.syncProductVersions(INVALID_AUTHORIZATION_HEADER, false)); assertEquals(ErrorCode.GITHUB_USER_UNAUTHORIZED.getHelpText(), exception.getMessage()); } + @Test + void testSyncOneProductInvalidProductPath() { + Product product = new Product(); + product.setId("a-trust"); + when(axonIvyMarketRepoService.getMarketItemByPath(any(String.class))).thenReturn(new ArrayList<>()); + var response = productController.syncOneProduct(AUTHORIZATION_HEADER, PRODUCT_ID_SAMPLE, + PRODUCT_PATH_SAMPLE, true); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + assertEquals(ErrorCode.PRODUCT_NOT_FOUND.getHelpText(), response.getBody().getMessageDetails()); + } + + @Test + void testSyncOneProductSuccess() { + Product product = new Product(); + product.setId("a-trust"); + GHContent content = mock(GHContent.class); + List contents = new ArrayList<>(); + contents.add(content); + when(axonIvyMarketRepoService.getMarketItemByPath(any(String.class))).thenReturn(contents); + when(service.syncOneProduct(any(String.class), any(String.class), any(Boolean.class))).thenReturn(true); + var response = productController.syncOneProduct(AUTHORIZATION_HEADER, PRODUCT_ID_SAMPLE, + PRODUCT_PATH_SAMPLE, true); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + assertEquals("Sync successfully!", response.getBody().getMessageDetails()); + } + + @Test + void testSyncOneProductInvalidToken() { + doThrow(new UnauthorizedException(ErrorCode.GITHUB_USER_UNAUTHORIZED.getCode(), + ErrorCode.GITHUB_USER_UNAUTHORIZED.getHelpText())).when(gitHubService) + .validateUserInOrganizationAndTeam(any(String.class), any(String.class), any(String.class)); + + UnauthorizedException exception = assertThrows(UnauthorizedException.class, + () -> productController.syncOneProduct(INVALID_AUTHORIZATION_HEADER, PRODUCT_ID_SAMPLE, + PRODUCT_PATH_SAMPLE, false)); + + assertEquals(ErrorCode.GITHUB_USER_UNAUTHORIZED.getHelpText(), exception.getMessage()); + } @Test void testCreateCustomSortProductsSuccess() { diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java index b949f3999..f3a6bd72e 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java @@ -13,6 +13,7 @@ import com.axonivy.market.service.ProductService; import com.axonivy.market.service.VersionService; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -283,4 +284,17 @@ private ProductJsonContent mockProductJsonContent() { return jsonContent; } + + @Test + void testGetLatestArtifactDownloadUrl() { + String mockDownloadUrl = "https://market.axonivy.com"; + when(versionService.getLatestVersionArtifactDownloadUrl(Mockito.anyString(),Mockito.anyString(), + Mockito.anyString())).thenReturn(StringUtils.EMPTY); + var response = productDetailsController.getLatestArtifactDownloadUrl("portal", "1.0.0", "portal-app.zip"); + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + when(versionService.getLatestVersionArtifactDownloadUrl(Mockito.anyString(),Mockito.anyString(), + Mockito.anyString())).thenReturn(mockDownloadUrl); + response = productDetailsController.getLatestArtifactDownloadUrl("portal", "1.0.0", "portal-app.zip"); + assertEquals(HttpStatus.OK, response.getStatusCode()); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/factory/VersionFactoryTest.java b/marketplace-service/src/test/java/com/axonivy/market/factory/VersionFactoryTest.java index 6244d0528..628b813b8 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/factory/VersionFactoryTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/factory/VersionFactoryTest.java @@ -1,5 +1,8 @@ package com.axonivy.market.factory; +import com.axonivy.market.BaseSetup; +import com.axonivy.market.enums.DevelopmentVersion; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @@ -9,7 +12,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @ExtendWith(MockitoExtension.class) -class VersionFactoryTest { +class VersionFactoryTest extends BaseSetup { final List mockVersions = List.of("10.0.0", "11.4.0-m1", "10.0.1-SNAPSHOT"); @@ -33,4 +36,20 @@ void testResolveVersion() { resolvedVersion = VersionFactory.get(mockVersions, "dev"); assertEquals("11.4.0-m1", resolvedVersion, "Should return highest dev release of that minor release"); } + @Test + void testGetFromMetadata() { + assertEquals(StringUtils.EMPTY, VersionFactory.getFromMetadata(List.of(), DevelopmentVersion.LATEST.getCode())); + assertEquals(StringUtils.EMPTY, VersionFactory.getFromMetadata(List.of(), DevelopmentVersion.DEV.getCode())); + + assertEquals(MOCK_RELEASED_VERSION, VersionFactory.getFromMetadata(List.of(getMockMetadataWithVersions()), + DevelopmentVersion.LATEST.getCode())); + assertEquals(MOCK_SPRINT_RELEASED_VERSION, VersionFactory.getFromMetadata(List.of(getMockMetadataWithVersions()), + DevelopmentVersion.DEV.getCode())); + assertEquals(MOCK_RELEASED_VERSION, VersionFactory.getFromMetadata(List.of(getMockMetadataWithVersions()), + "10-dev")); + assertEquals(MOCK_RELEASED_VERSION, VersionFactory.getFromMetadata(List.of(getMockMetadataWithVersions()), + "10")); + assertEquals(MOCK_RELEASED_VERSION, VersionFactory.getFromMetadata(List.of(getMockMetadataWithVersions()), + "10.0")); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/repository/impl/CustomProductRepositoryImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/repository/impl/CustomProductRepositoryImplTest.java index 6a202229c..4ed738baf 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/repository/impl/CustomProductRepositoryImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/repository/impl/CustomProductRepositoryImplTest.java @@ -4,7 +4,8 @@ import com.axonivy.market.constants.MongoDBConstants; import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductDesignerInstallation; -import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.repository.MavenArtifactVersionRepository; +import com.axonivy.market.repository.MetadataRepository; import com.axonivy.market.repository.ProductModuleContentRepository; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -21,20 +22,20 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class CustomProductRepositoryImplTest extends BaseSetup { - private static final String ID = "bmpn-statistic"; - private static final String TAG = "v10.0.21"; @Mock ProductModuleContentRepository contentRepo; private Product mockProduct; private Aggregation mockAggregation; @Mock private MongoTemplate mongoTemplate; - + @Mock + private MetadataRepository metadataRepo; + @Mock + private MavenArtifactVersionRepository mavenArtifactVersionRepo; @InjectMocks private CustomProductRepositoryImpl repo; @@ -53,7 +54,7 @@ private void setUpMockAggregateResult() { when(mongoTemplate.aggregate(any(Aggregation.class), eq(MongoDBConstants.PRODUCT_COLLECTION), eq(Product.class))).thenReturn(aggregationResults); mockProduct = new Product(); - mockProduct.setId(ID); + mockProduct.setId(MOCK_PRODUCT_ID); when(aggregationResults.getUniqueMappedResult()).thenReturn(mockProduct); } @@ -78,70 +79,39 @@ void testReleasedVersionsById_WhenResultIsNull() { eq(Product.class))).thenReturn(aggregationResults); when(aggregationResults.getUniqueMappedResult()).thenReturn(null); - List results = repo.getReleasedVersionsById(ID); + List results = repo.getReleasedVersionsById(MOCK_PRODUCT_ID); assertEquals(0, results.size()); } @Test void testGetProductById() { setUpMockAggregateResult(); - Product actualProduct = repo.getProductById(ID); - assertEquals(mockProduct, actualProduct); - } - - @Test - void testGetProductById_andFindProductModuleContentByNewestVersion() { - mockAggregation = mock(Aggregation.class); - AggregationResults aggregationResults = mock(AggregationResults.class); - - when(mongoTemplate.aggregate(any(Aggregation.class), eq(MongoDBConstants.PRODUCT_COLLECTION), - eq(Product.class))).thenReturn(aggregationResults); - - ProductModuleContent productModuleContent = ProductModuleContent.builder() - .productId("bmpn-statistic") - .tag("v11.3.0") - .build(); - - when(contentRepo.findByTagAndProductId("v11.3.0", ID)).thenReturn(productModuleContent); - - mockProduct = Product.builder() - .id(ID) - .newestReleaseVersion("12.0.0-m264") - .releasedVersions(List.of("11.1.1", "11.1.0", "11.3.0")) - .productModuleContent(productModuleContent) - .build(); - - when(aggregationResults.getUniqueMappedResult()).thenReturn(mockProduct); - - Product actualProduct = repo.getProductByIdWithNewestReleaseVersion(ID,false); - - verify(contentRepo, times(1)).findByTagAndProductId("v11.3.0", ID); + Product actualProduct = repo.getProductById(MOCK_PRODUCT_ID); assertEquals(mockProduct, actualProduct); } @Test void testGetProductByIdAndTag() { setUpMockAggregateResult(); - Product actualProduct = repo.getProductByIdWithTagOrVersion(ID, TAG); + Product actualProduct = repo.getProductByIdWithTagOrVersion(MOCK_PRODUCT_ID, MOCK_TAG_FROM_RELEASED_VERSION); assertEquals(mockProduct, actualProduct); } @Test void testGetReleasedVersionsById() { setUpMockAggregateResult(); - List actualReleasedVersions = repo.getReleasedVersionsById(ID); + List actualReleasedVersions = repo.getReleasedVersionsById(MOCK_PRODUCT_ID); assertEquals(mockProduct.getReleasedVersions(), actualReleasedVersions); } @Test void testIncreaseInstallationCount() { - String productId = "testProductId"; Product product = new Product(); - product.setId(productId); + product.setId(MOCK_PRODUCT_ID); product.setInstallationCount(5); when(mongoTemplate.findAndModify(any(Query.class), any(Update.class), any(FindAndModifyOptions.class), eq(Product.class))).thenReturn(product); - int updatedCount = repo.increaseInstallationCount(productId); + int updatedCount = repo.increaseInstallationCount(MOCK_PRODUCT_ID); assertEquals(5, updatedCount); verify(mongoTemplate).findAndModify(any(Query.class), any(Update.class), any(FindAndModifyOptions.class), eq(Product.class)); @@ -151,7 +121,7 @@ void testIncreaseInstallationCount() { void testIncreaseInstallationCount_NullProduct() { when(mongoTemplate.findAndModify(any(Query.class), any(Update.class), any(FindAndModifyOptions.class), eq(Product.class))).thenReturn(null); - int updatedCount = repo.increaseInstallationCount(ID); + int updatedCount = repo.increaseInstallationCount(MOCK_PRODUCT_ID); assertEquals(0, updatedCount); } @@ -159,15 +129,15 @@ void testIncreaseInstallationCount_NullProduct() { void testUpdateInitialCount() { setUpMockAggregateResult(); int initialCount = 10; - repo.updateInitialCount(ID, initialCount); + repo.updateInitialCount(MOCK_PRODUCT_ID, initialCount); verify(mongoTemplate).updateFirst(any(Query.class), - eq(new Update().inc("InstallationCount", initialCount).set("SynchronizedInstallationCount", true)), + eq(new Update().inc(MongoDBConstants.INSTALLATION_COUNT, initialCount).set(MongoDBConstants.SYNCHRONIZED_INSTALLATION_COUNT, true)), eq(Product.class)); } @Test void testIncreaseInstallationCountForProductByDesignerVersion() { - repo.increaseInstallationCountForProductByDesignerVersion("portal", "10.0.20"); + repo.increaseInstallationCountForProductByDesignerVersion(MOCK_PRODUCT_ID, MOCK_RELEASED_VERSION); verify(mongoTemplate).upsert(any(Query.class), any(Update.class), eq(ProductDesignerInstallation.class)); } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ExternalDocumentServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ExternalDocumentServiceImplTest.java index a5d7f1cc6..0e0eb5412 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ExternalDocumentServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ExternalDocumentServiceImplTest.java @@ -6,7 +6,6 @@ import com.axonivy.market.repository.ExternalDocumentMetaRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.FileDownloadService; -import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -47,7 +46,7 @@ void testSyncDocumentForProduct() throws IOException { verify(externalDocumentMetaRepository, times(0)).findByProductIdAndVersion(any(), any()); when(productRepository.findById(PORTAL)).thenReturn(mockPortalProduct()); - service.syncDocumentForProduct(PORTAL, true); + service.syncDocumentForProduct(PORTAL, false); verify(externalDocumentMetaRepository, times(2)).findByProductIdAndVersion(any(), any()); when(fileDownloadService.downloadAndUnzipFile(any(), anyBoolean())).thenReturn("data" + RELATIVE_LOCATION); @@ -72,17 +71,17 @@ void testFindExternalDocumentURI() { var mockVersion = "10.0.0"; var mockProductDocumentMeta = new ExternalDocumentMeta(); when(productRepository.findById(PORTAL)).thenReturn(mockPortalProduct()); - var result = service.findExternalDocumentURI(PORTAL, mockVersion); + var result = service.findExternalDocument(PORTAL, mockVersion); verify(productRepository, times(1)).findById(any()); - assertTrue(StringUtils.isEmpty(result)); + assertNull(result); mockProductDocumentMeta.setProductId(PORTAL); mockProductDocumentMeta.setVersion(mockVersion); mockProductDocumentMeta.setRelativeLink(RELATIVE_LOCATION); when(externalDocumentMetaRepository.findByProductId(PORTAL)).thenReturn(List.of(mockProductDocumentMeta)); - result = service.findExternalDocumentURI(PORTAL, mockVersion); + result = service.findExternalDocument(PORTAL, mockVersion); assertNotNull(result); - assertTrue(result.contains("/index.html")); + assertTrue(result.getRelativeLink().contains("/index.html")); } private Optional mockPortalProduct() { diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyMarketRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyMarketRepoServiceImplTest.java index fc3406146..62f0c48c4 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyMarketRepoServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyMarketRepoServiceImplTest.java @@ -112,4 +112,14 @@ void testGetLastCommit() { var lastCommit = axonIvyMarketRepoServiceImpl.getLastCommit(0L); assertNull(lastCommit); } + + @Test + void testGetMarketItemByPath() throws IOException { + var mockGHContent = mock(GHContent.class); + List mockGhContents = new ArrayList<>(); + mockGhContents.add(mockGHContent); + when(gitHubService.getDirectoryContent(any(), any(), any())).thenReturn(mockGhContents); + var ghContents = axonIvyMarketRepoServiceImpl.getMarketItemByPath("market/connector/a-trust"); + assertEquals(1, ghContents.size()); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyProductRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyProductRepoServiceImplTest.java index e047768de..d554ad6d8 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyProductRepoServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyProductRepoServiceImplTest.java @@ -7,6 +7,7 @@ import com.axonivy.market.constants.ProductJsonConstants; import com.axonivy.market.constants.ReadmeConstants; import com.axonivy.market.entity.Image; +import com.axonivy.market.entity.Product; import com.axonivy.market.enums.Language; import com.axonivy.market.github.service.GitHubService; import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; @@ -14,6 +15,7 @@ import com.axonivy.market.repository.ProductJsonContentRepository; import com.axonivy.market.service.ImageService; import com.axonivy.market.util.MavenUtils; +import com.axonivy.market.util.ProductContentUtils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -38,11 +40,18 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; - +import java.util.Map; + +import static com.axonivy.market.constants.GitHubConstants.MG_GRAPH_IMAGES_FOR_SETUP_FILE; +import static com.axonivy.market.constants.GitHubConstants.MS_GRAPH_PRODUCT_DIRECTORY; +import static com.axonivy.market.constants.MongoDBConstants.TAG; +import static com.axonivy.market.constants.ProductJsonConstants.EN_LANGUAGE; +import static com.axonivy.market.constants.ReadmeConstants.SETUP_FILE; +import static com.axonivy.market.enums.NonStandardProduct.MICROSOFT_TEAMS; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -85,11 +94,6 @@ void setup() throws IOException { when(mockGHOrganization.getRepository(any())).thenReturn(ghRepository); } - private static InputStream getMockInputStream() { - String jsonContent = getMockProductJsonContent().getContent(); - return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); - } - private static GHContent createMockImage() { GHContent mockImage = mock(GHContent.class); when(mockImage.isFile()).thenReturn(true); @@ -228,7 +232,7 @@ void testGetReadmeAndProductContentFromTag_ImageFromRootFolder() { when(mockImageFile.getName()).thenReturn(IMAGE_NAME); when(imageService.mappingImageFromGHContent(any(), any(), anyBoolean())).thenReturn(mockImage()); - String updatedReadme = axonivyProductRepoServiceImpl.updateImagesWithDownloadUrl(getMockProducts().get(0), + String updatedReadme = axonivyProductRepoServiceImpl.updateImagesWithDownloadUrl(BaseSetup.MOCK_PRODUCT_ID, List.of(mockImageFile), readmeContentWithImageFolder); assertEquals(""" @@ -270,7 +274,7 @@ void testGetReadmeAndProductContentFromTag_ImageFromChildFolder() throws IOExcep when(imageService.mappingImageFromGHContent(any(), any(), anyBoolean())).thenReturn(mockImage()); - String updatedReadme = axonivyProductRepoServiceImpl.updateImagesWithDownloadUrl(getMockProducts().get(0), + String updatedReadme = axonivyProductRepoServiceImpl.updateImagesWithDownloadUrl(BaseSetup.MOCK_PRODUCT_ID, List.of(mockImageFile), readmeContentWithImageFolder); assertEquals(""" @@ -391,4 +395,48 @@ void testExtractedContentStream() { assertNull(GitHubUtils.extractedContentStream(null)); assertNull(GitHubUtils.extractedContentStream(content)); } + + @Test + void testUpdateProductModuleContentSetupFromSetupMd() throws IOException { + when(gitHubService.getRepository(anyString())).thenReturn(ghRepository); + + InputStream mockReadmeInputStream = mock(InputStream.class); + + String setupStringContent = getMockSetupMd(); + + GHContent setupFileContent = mock(GHContent.class); + when(setupFileContent.isFile()).thenReturn(true); + when(setupFileContent.getName()).thenReturn(SETUP_FILE); + when(setupFileContent.read()).thenReturn(mockReadmeInputStream); + when(mockReadmeInputStream.readAllBytes()).thenReturn(setupStringContent.getBytes()); + + GHContent setupImageContent = mock(GHContent.class); + when(setupImageContent.isDirectory()).thenReturn(true); + when(setupImageContent.getName()).thenReturn(MG_GRAPH_IMAGES_FOR_SETUP_FILE); + + GHContent mockImageFile3 = mock(GHContent.class); + when(mockImageFile3.getName()).thenReturn(IMAGE_NAME); + + PagedIterable pagedIterable = mock(String.valueOf(GHContent.class)); + when(setupImageContent.listDirectoryContent()).thenReturn(pagedIterable); + when(pagedIterable.toList()).thenReturn(List.of(mockImageFile3)); + + when(ghRepository.getDirectoryContent(MS_GRAPH_PRODUCT_DIRECTORY, TAG)) + .thenReturn(List.of(setupFileContent, setupImageContent)); + + when(imageService.mappingImageFromGHContent(any(), any(), anyBoolean())).thenReturn(mockImage()); + + Product mockProduct = Product.builder() + .id(MICROSOFT_TEAMS.getId()) + .repositoryName("market/connector/microsoft365/chat/") + .build(); + Map> moduleContents = new HashMap<>(); + + moduleContents.put(ProductContentUtils.SETUP, new HashMap<>(Map.of(EN_LANGUAGE, "setup file content"))); + + axonivyProductRepoServiceImpl.updateSetupPartForProductModuleContent(mockProduct, moduleContents, TAG); + + assertTrue(moduleContents.get(ProductContentUtils.SETUP).get(EN_LANGUAGE).contains(ProductContentUtils.removeFirstLine( + setupStringContent.replace(IMAGE_NAME, "imageId-66e2b14868f2f95b2f95549a")))); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/GitHubServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GitHubServiceImplTest.java index 1cfd8f00d..be7f38edc 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/GitHubServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GitHubServiceImplTest.java @@ -1,14 +1,12 @@ package com.axonivy.market.service.impl; import com.axonivy.market.github.service.impl.GitHubServiceImpl; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHRepository; 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; @@ -16,7 +14,6 @@ import java.io.IOException; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -36,12 +33,6 @@ class GitHubServiceImplTest { @InjectMocks private GitHubServiceImpl gitHubService; - @BeforeEach - public void setUp() { - MockitoAnnotations.openMocks(this); - lenient().when(restTemplateBuilder.build()).thenReturn(restTemplate); - } - @Test void testGetGithub() throws IOException { var result = gitHubService.getGitHub(); @@ -63,4 +54,10 @@ void testGetDirectoryContent() throws IOException { var result = gitHubService.getDirectoryContent(ghRepository, "", ""); assertEquals(0, result.size()); } + + @Test + void testGithubWithToken() throws IOException { + var result = gitHubService.getGitHub("accessToken"); + assertEquals(DUMMY_API_URL, result.getApiUrl()); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ImageServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ImageServiceImplTest.java index 5b22b8cda..5b5cb746f 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ImageServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ImageServiceImplTest.java @@ -1,8 +1,9 @@ package com.axonivy.market.service.impl; +import com.axonivy.market.BaseSetup; import com.axonivy.market.entity.Image; -import com.axonivy.market.entity.Product; import com.axonivy.market.repository.ImageRepository; +import com.axonivy.market.service.FileDownloadService; import com.axonivy.market.util.MavenUtils; import org.bson.types.Binary; import org.junit.jupiter.api.Test; @@ -37,13 +38,16 @@ import static org.mockito.Mockito.never; @ExtendWith(MockitoExtension.class) -class ImageServiceImplTest { +class ImageServiceImplTest extends BaseSetup { @Captor ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Image.class); @InjectMocks private ImageServiceImpl imageService; @Mock private ImageRepository imageRepository; + @Mock + private FileDownloadService fileDownloadService; + public static final String GOOGLE_MAPS_CONNECTOR = "google-maps-connector"; @Test void testMappingImageFromGHContent() throws IOException { @@ -54,7 +58,7 @@ void testMappingImageFromGHContent() throws IOException { InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); when(content.read()).thenReturn(inputStream); - imageService.mappingImageFromGHContent(mockProduct(), content, true); + imageService.mappingImageFromGHContent(GOOGLE_MAPS_CONNECTOR, content, true); Image expectedImage = new Image(); expectedImage.setProductId("google-maps-connector"); @@ -68,31 +72,47 @@ void testMappingImageFromGHContent() throws IOException { assertEquals(argumentCaptor.getValue().getImageUrl(), expectedImage.getImageUrl()); when(imageRepository.findByProductIdAndSha(anyString(), anyString())).thenReturn(expectedImage); - Image result = imageService.mappingImageFromGHContent(mockProduct(), content, false); + Image result = imageService.mappingImageFromGHContent(GOOGLE_MAPS_CONNECTOR, content, false); assertEquals(expectedImage, result); } + @Test + void testMappingImageFromGHContent_getImageFromDownloadUrl() throws IOException { + GHContent content = mock(GHContent.class); + when(content.getSha()).thenReturn("914d9b6956db7a1404622f14265e435f36db81fa"); + when(content.getDownloadUrl()).thenReturn(MOCK_MAVEN_URL); + + when(content.read()).thenThrow(new UnsupportedOperationException("Unrecognized encoding")); + when(fileDownloadService.downloadFile(MOCK_MAVEN_URL)).thenReturn("content".getBytes()); + + imageService.mappingImageFromGHContent(GOOGLE_MAPS_CONNECTOR, content, false); + + verify(imageRepository).save(argumentCaptor.capture()); + verify(fileDownloadService, times(1)).downloadFile(MOCK_MAVEN_URL); + assertEquals(new Binary("content".getBytes()), argumentCaptor.getValue().getImageData()); + + } + @Test void testMappingImageFromDownloadedFolder() { try (MockedStatic mockedMavenUtils = Mockito.mockStatic(MavenUtils.class)) { - Product product = new Product(); - product.setId("connectivity-demo"); + String productId = "connectivity-demo"; byte[] newImageData = "connectivity-image-data".getBytes(); Path imagePath = Path.of("connectivity-image.png"); ByteArrayInputStream inputStream = new ByteArrayInputStream(newImageData); mockedMavenUtils.when(() -> MavenUtils.extractedContentStream(imagePath)).thenReturn(inputStream); - when(imageRepository.findByProductId(product.getId())).thenReturn(Collections.emptyList()); + when(imageRepository.findByProductId(productId)).thenReturn(Collections.emptyList()); Image newImage = new Image(); newImage.setImageData(new Binary(newImageData)); - newImage.setProductId(product.getId()); + newImage.setProductId(productId); when(imageRepository.save(any(Image.class))).thenReturn(newImage); - Image result = imageService.mappingImageFromDownloadedFolder(product, imagePath); + Image result = imageService.mappingImageFromDownloadedFolder(productId, imagePath); assertNotNull(result); assertEquals(newImage, result); @@ -103,8 +123,7 @@ void testMappingImageFromDownloadedFolder() { @Test void testMappingImageFromDownloadedFolderWhenImageExists() { try (MockedStatic mockedMavenUtils = Mockito.mockStatic(MavenUtils.class)) { - Product product = new Product(); - product.setId("connectivity-demo"); + String productId = "connectivity-demo"; byte[] existingImageData = "connectivity-image-data".getBytes(); byte[] newImageData = "connectivity-image-data".getBytes(); @@ -115,11 +134,11 @@ void testMappingImageFromDownloadedFolderWhenImageExists() { Image existingImage = new Image(); existingImage.setImageData(new Binary(existingImageData)); - existingImage.setProductId(product.getId()); + existingImage.setProductId(productId); - when(imageRepository.findByProductId(product.getId())).thenReturn(List.of(existingImage)); + when(imageRepository.findByProductId(productId)).thenReturn(List.of(existingImage)); - Image result = imageService.mappingImageFromDownloadedFolder(product, imagePath); + Image result = imageService.mappingImageFromDownloadedFolder(productId, imagePath); assertNotNull(result); assertEquals(existingImage, result); @@ -130,13 +149,12 @@ void testMappingImageFromDownloadedFolderWhenImageExists() { @Test void testMappingImageFromDownloadedFolder_ReturnNull() { try (MockedStatic mockedMavenUtils = Mockito.mockStatic(MavenUtils.class)) { - Product product = new Product(); - product.setId("connectivity-demo"); + String productId = "connectivity-demo"; Path imagePath = Path.of("connectivity-image.png"); mockedMavenUtils.when(() -> MavenUtils.extractedContentStream(imagePath)).thenThrow( new NullPointerException("File not found")); - Image result = imageService.mappingImageFromDownloadedFolder(product, imagePath); + Image result = imageService.mappingImageFromDownloadedFolder(productId, imagePath); assertNull(result); verify(imageRepository, times(0)).save(any(Image.class)); @@ -145,13 +163,7 @@ void testMappingImageFromDownloadedFolder_ReturnNull() { @Test void testMappingImageFromGHContent_noGhContent() { - var result = imageService.mappingImageFromGHContent(mockProduct(), null, true); + var result = imageService.mappingImageFromGHContent(GOOGLE_MAPS_CONNECTOR, null, true); assertNull(result); } - - private Product mockProduct() { - return Product.builder().id("google-maps-connector") - .language("English") - .build(); - } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java index 3982eac1f..897b97ddc 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java @@ -5,6 +5,7 @@ import com.axonivy.market.criteria.ProductSearchCriteria; import com.axonivy.market.entity.GitHubRepoMeta; import com.axonivy.market.entity.MavenArtifactVersion; +import com.axonivy.market.entity.Metadata; import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductCustomSort; import com.axonivy.market.entity.ProductModuleContent; @@ -23,12 +24,15 @@ import com.axonivy.market.repository.GitHubRepoMetaRepository; import com.axonivy.market.repository.ImageRepository; import com.axonivy.market.repository.MavenArtifactVersionRepository; +import com.axonivy.market.repository.MetadataRepository; +import com.axonivy.market.repository.MetadataSyncRepository; import com.axonivy.market.repository.ProductCustomSortRepository; +import com.axonivy.market.repository.ProductJsonContentRepository; import com.axonivy.market.repository.ProductModuleContentRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.ImageService; -import com.axonivy.market.util.MavenUtils; -import com.axonivy.market.util.VersionUtils; +import com.axonivy.market.service.MetadataService; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -40,8 +44,6 @@ import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -57,7 +59,6 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -71,19 +72,17 @@ import static com.axonivy.market.enums.DocumentField.SHORT_DESCRIPTIONS; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ProductServiceImplTest extends BaseSetup { - - public static final String RELEASE_TAG = "v10.0.2"; private static final long LAST_CHANGE_TIME = 1718096290000L; private static final Pageable PAGEABLE = PageRequest.of(0, 20, Sort.by(SortOption.ALPHABETICALLY.getOption()).descending()); private static final String SHA1_SAMPLE = "35baa89091b2452b77705da227f1a964ecabc6c8"; private static final String INSTALLATION_FILE_PATH = "src/test/resources/installationCount.json"; private static final String EMPTY_SOURCE_URL_META_JSON_FILE = "/emptySourceUrlMeta.json"; + private static final String META_JSON_FILE_WITH_VENDOR_INFORMATION = "/meta-with-vendor-information.json"; @Captor ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Product.class); @Captor @@ -102,21 +101,27 @@ class ProductServiceImplTest extends BaseSetup { @Mock private GHRepository ghRepository; @Mock - private ProductRepository productRepository; + private ProductRepository productRepo; + @Mock + private ProductModuleContentRepository productModuleContentRepo; @Mock - private ProductModuleContentRepository productModuleContentRepository; + private ProductJsonContentRepository productJsonContentRepo; @Mock private GHAxonIvyMarketRepoService marketRepoService; @Mock - private GitHubRepoMetaRepository repoMetaRepository; + private GitHubRepoMetaRepository repoMetaRepo; @Mock private GitHubService gitHubService; - @Mock - private ImageRepository imageRepository; - + private MetadataService metadataService; + @Mock + private ImageRepository imageRepo; @Mock - private ProductCustomSortRepository productCustomSortRepository; + private MetadataRepository metadataRepo; + @Mock + private MetadataSyncRepository metadataSyncRepo; + @Mock + private ProductCustomSortRepository productCustomSortRepo; @Mock private GHAxonIvyProductRepoService ghAxonIvyProductRepoService; @Mock @@ -133,17 +138,18 @@ public void setup() { @Test void testUpdateInstallationCountForProduct() { - String designerVersion = "10.0.20"; - int result = productService.updateInstallationCountForProduct(null, designerVersion); + int result = productService.updateInstallationCountForProduct(null, MOCK_RELEASED_VERSION); assertEquals(0, result); - Product product = mockProduct(); - when(productRepository.getProductById(product.getId())).thenReturn(product); - when(productRepository.increaseInstallationCount(product.getId())).thenReturn(31); - result = productService.updateInstallationCountForProduct(product.getId(), designerVersion); + Product product = getMockProduct(); + product.setSynchronizedInstallationCount(true); + when(productRepo.getProductById(MOCK_PRODUCT_ID)).thenReturn(product); + when(productRepo.increaseInstallationCount(MOCK_PRODUCT_ID)).thenReturn(31); + + result = productService.updateInstallationCountForProduct(MOCK_PRODUCT_ID, MOCK_RELEASED_VERSION); assertEquals(31, result); - result = productService.updateInstallationCountForProduct(product.getId(), ""); + result = productService.updateInstallationCountForProduct(MOCK_PRODUCT_ID, StringUtils.EMPTY); assertEquals(31, result); } @@ -151,8 +157,8 @@ void testUpdateInstallationCountForProduct() { void testSyncInstallationCountWithNewProduct() { Product product = new Product(); product.setSynchronizedInstallationCount(null); - product.setId("portal"); - ReflectionTestUtils.setField(productService, "legacyInstallationCountPath", INSTALLATION_FILE_PATH); + product.setId(MOCK_PRODUCT_ID); + ReflectionTestUtils.setField(productService, LEGACY_INSTALLATION_COUNT_PATH_FIELD_NAME, INSTALLATION_FILE_PATH); productService.syncInstallationCountWithProduct(product); @@ -162,8 +168,8 @@ void testSyncInstallationCountWithNewProduct() { @Test void testSyncInstallationCountWithProduct() { - ReflectionTestUtils.setField(productService, "legacyInstallationCountPath", INSTALLATION_FILE_PATH); - Product product = mockProduct(); + ReflectionTestUtils.setField(productService, LEGACY_INSTALLATION_COUNT_PATH_FIELD_NAME, INSTALLATION_FILE_PATH); + Product product = getMockProduct(); product.setSynchronizedInstallationCount(false); productService.syncInstallationCountWithProduct(product); @@ -172,16 +178,11 @@ void testSyncInstallationCountWithProduct() { assertTrue(product.getSynchronizedInstallationCount()); } - private Product mockProduct() { - return Product.builder().id("google-maps-connector").language("English").synchronizedInstallationCount(true) - .build(); - } - @Test void testFindProducts() { language = "en"; // Start testing by All - when(productRepository.searchByCriteria(any(), any(Pageable.class))).thenReturn(mockResultReturn); + when(productRepo.searchByCriteria(any(), any(Pageable.class))).thenReturn(mockResultReturn); // Executes var result = productService.findProducts(TypeOption.ALL.getOption(), keyword, language, false, PAGEABLE); assertEquals(mockResultReturn, result); @@ -200,7 +201,7 @@ void testFindProducts() { @Test void testFindProductsInRESTClientOfDesigner() { productService.findProducts(TypeOption.CONNECTORS.getOption(), keyword, Language.EN.getValue(), true, PAGEABLE); - verify(productRepository).searchByCriteria(productSearchCriteriaArgumentCaptor.capture(), any(Pageable.class)); + verify(productRepo).searchByCriteria(productSearchCriteriaArgumentCaptor.capture(), any(Pageable.class)); assertEquals(List.of(SHORT_DESCRIPTIONS), productSearchCriteriaArgumentCaptor.getValue().getExcludeFields()); } @@ -220,7 +221,7 @@ void testSyncProductsAsUpdateMetaJSONFromGitHub() throws IOException { var mockGHContent = mockGHContentAsMetaJSON(); when(gitHubService.getGHContent(any(), anyString(), any())).thenReturn(mockGHContent); when(mockGHContent.read()).thenReturn(this.getClass().getResourceAsStream(EMPTY_SOURCE_URL_META_JSON_FILE)); - when(productRepository.save(any(Product.class))).thenReturn(new Product()); + when(productRepo.save(any(Product.class))).thenReturn(new Product()); // Executes var result = productService.syncLatestDataFromMarketRepo(); @@ -251,8 +252,6 @@ void testSyncProductsAsUpdateLogoFromGitHub() throws IOException { mockGitHubFile.setType(FileType.LOGO); mockGitHubFile.setStatus(FileStatus.ADDED); when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGitHubFile)); - var mockGHContent = mockGHContentAsMetaJSON(); - when(gitHubService.getGHContent(any(), anyString(), any())).thenReturn(mockGHContent); // Executes var result = productService.syncLatestDataFromMarketRepo(); @@ -273,14 +272,14 @@ void testSyncProductsAsUpdateLogoFromGitHub() throws IOException { @Test void testFindAllProductsWithKeyword() { language = "en"; - when(productRepository.searchByCriteria(any(), any(Pageable.class))).thenReturn(mockResultReturn); + when(productRepo.searchByCriteria(any(), any(Pageable.class))).thenReturn(mockResultReturn); // Executes var result = productService.findProducts(TypeOption.ALL.getOption(), keyword, language, false, PAGEABLE); assertEquals(mockResultReturn, result); - verify(productRepository).searchByCriteria(any(), any(Pageable.class)); + verify(productRepo).searchByCriteria(any(), any(Pageable.class)); // Test has keyword - when(productRepository.searchByCriteria(any(), any(Pageable.class))) + when(productRepo.searchByCriteria(any(), any(Pageable.class))) .thenReturn(new PageImpl<>(mockResultReturn.stream() .filter(product -> product.getNames().get(Language.EN.getValue()).equals(SAMPLE_PRODUCT_NAME)) .toList())); @@ -290,7 +289,7 @@ void testFindAllProductsWithKeyword() { assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getNames().get(Language.EN.getValue())); // Test has keyword and type is connector - when(productRepository.searchByCriteria(any(), any(Pageable.class))) + when(productRepo.searchByCriteria(any(), any(Pageable.class))) .thenReturn(new PageImpl<>(mockResultReturn.stream() .filter(product -> product.getNames().get(Language.EN.getValue()).equals(SAMPLE_PRODUCT_NAME) && product.getType().equals(TypeOption.CONNECTORS.getCode())) @@ -306,7 +305,7 @@ void testFindAllProductsWithKeyword() { void testSyncProductsFirstTime() throws IOException { var mockCommit = mockGHCommitHasSHA1(SHA1_SAMPLE); when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); - when(repoMetaRepository.findByRepoName(anyString())).thenReturn(null); + when(repoMetaRepo.findByRepoName(anyString())).thenReturn(null); when(ghAxonIvyProductRepoService.getReadmeAndProductContentsFromTag(any(), any(), anyString())).thenReturn( mockReadmeProductContent()); when(gitHubService.getRepository(any())).thenReturn(ghRepository); @@ -314,30 +313,23 @@ void testSyncProductsFirstTime() throws IOException { GHTag mockTag = mock(GHTag.class); GHCommit mockGHCommit = mock(GHCommit.class); - when(mockTag.getName()).thenReturn(RELEASE_TAG); + when(mockTag.getName()).thenReturn(MOCK_TAG_FROM_RELEASED_VERSION); when(mockTag.getCommit()).thenReturn(mockGHCommit); when(mockGHCommit.getCommitDate()).thenReturn(new Date()); when(gitHubService.getRepositoryTags(anyString())).thenReturn(List.of(mockTag)); - var mockContent = mockGHContentAsMetaJSON(); - InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); - when(mockContent.read()).thenReturn(inputStream); - - var mockContentLogo = mockGHContentAsLogo(); - List mockMetaJsonAndLogoList = new ArrayList<>(List.of(mockContent, mockContentLogo)); - Map> mockGHContentMap = new HashMap<>(); - mockGHContentMap.put(SAMPLE_PRODUCT_ID, mockMetaJsonAndLogoList); + mockGHContentMap.put(SAMPLE_PRODUCT_ID, mockMetaJsonAndLogoList()); when(marketRepoService.fetchAllMarketItems()).thenReturn(mockGHContentMap); - when(productModuleContentRepository.saveAll(anyList())).thenReturn(List.of(mockReadmeProductContent())); + when(productModuleContentRepo.saveAll(anyList())).thenReturn(List.of(mockReadmeProductContent())); when(imageService.mappingImageFromGHContent(any(), any(), anyBoolean())) .thenReturn(GHAxonIvyProductRepoServiceImplTest.mockImage()); - when(productRepository.save(any(Product.class))).thenReturn(new Product()); + when(productRepo.save(any(Product.class))).thenReturn(new Product()); // Executes productService.syncLatestDataFromMarketRepo(); - verify(productModuleContentRepository).saveAll(argumentCaptorProductModuleContents.capture()); - verify(productRepository).save(argumentCaptor.capture()); + verify(productModuleContentRepo).saveAll(argumentCaptorProductModuleContents.capture()); + verify(productRepo).save(argumentCaptor.capture()); assertEquals(1, argumentCaptorProductModuleContents.getValue().size()); assertThat(argumentCaptorProductModuleContents.getValue().get(0).getId()) @@ -348,7 +340,7 @@ void testSyncProductsFirstTime() throws IOException { void testSyncProductsFirstTimeWithOutSourceUrl() throws IOException { var mockCommit = mockGHCommitHasSHA1(SHA1_SAMPLE); when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); - when(repoMetaRepository.findByRepoName(anyString())).thenReturn(null); + when(repoMetaRepo.findByRepoName(anyString())).thenReturn(null); var mockContent = mockGHContentAsMetaJSON(); InputStream inputStream = this.getClass().getResourceAsStream(EMPTY_SOURCE_URL_META_JSON_FILE); @@ -362,33 +354,30 @@ void testSyncProductsFirstTimeWithOutSourceUrl() throws IOException { when(marketRepoService.fetchAllMarketItems()).thenReturn(mockGHContentMap); when(imageService.mappingImageFromGHContent(any(), any(), anyBoolean())).thenReturn( GHAxonIvyProductRepoServiceImplTest.mockImage()); - when(productRepository.save(any(Product.class))).thenReturn(new Product()); + when(productRepo.save(any(Product.class))).thenReturn(new Product()); // Executes productService.syncLatestDataFromMarketRepo(); - verify(productModuleContentRepository).save(argumentCaptorProductModuleContent.capture()); + verify(productModuleContentRepo).save(argumentCaptorProductModuleContent.capture()); assertEquals("1.0", argumentCaptorProductModuleContent.getValue().getTag()); } @Test void testSyncProductsSecondTime() throws IOException { + Product mockProduct = getMockProduct(); + mockProduct.setProductModuleContent(mockReadmeProductContent()); + mockProduct.setRepositoryName("axonivy-market/bpmn-statistic"); var gitHubRepoMeta = mock(GitHubRepoMeta.class); when(gitHubRepoMeta.getLastSHA1()).thenReturn(SHA1_SAMPLE); var mockCommit = mockGHCommitHasSHA1(SHA1_SAMPLE); when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); - when(repoMetaRepository.findByRepoName(anyString())).thenReturn(gitHubRepoMeta); - - when(productRepository.findAll()).thenReturn(mockProducts()); - - GHCommit mockGHCommit = mock(GHCommit.class); + when(repoMetaRepo.findByRepoName(anyString())).thenReturn(gitHubRepoMeta); + when(productRepo.findAll()).thenReturn(List.of(mockProduct)); GHTag mockTag = mock(GHTag.class); - when(mockTag.getName()).thenReturn("v10.0.2"); + when(mockTag.getName()).thenReturn(MOCK_TAG_FROM_RELEASED_VERSION); GHTag mockTag2 = mock(GHTag.class); when(mockTag2.getName()).thenReturn("v10.0.3"); - when(mockTag2.getCommit()).thenReturn(mockGHCommit); - - when(mockGHCommit.getCommitDate()).thenReturn(new Date()); when(gitHubService.getRepositoryTags(anyString())).thenReturn(Arrays.asList(mockTag, mockTag2)); ProductModuleContent mockReturnProductContent = mockReadmeProductContent(); @@ -396,14 +385,14 @@ void testSyncProductsSecondTime() throws IOException { when(ghAxonIvyProductRepoService.getReadmeAndProductContentsFromTag(any(), any(), anyString())) .thenReturn(mockReturnProductContent); - when(productModuleContentRepository.saveAll(anyList())) + when(productModuleContentRepo.saveAll(anyList())) .thenReturn(List.of(mockReadmeProductContent(), mockReturnProductContent)); // Executes productService.syncLatestDataFromMarketRepo(); - verify(productModuleContentRepository).saveAll(argumentCaptorProductModuleContents.capture()); - verify(productRepository).save(argumentCaptor.capture()); + verify(productModuleContentRepo).saveAll(argumentCaptorProductModuleContents.capture()); + verify(productRepo).save(argumentCaptor.capture()); assertThat(argumentCaptor.getValue().getProductModuleContent().getId()) .isEqualTo(mockReadmeProductContent().getId()); } @@ -414,7 +403,7 @@ void testNothingToSync() { when(gitHubRepoMeta.getLastSHA1()).thenReturn(SHA1_SAMPLE); var mockCommit = mockGHCommitHasSHA1(SHA1_SAMPLE); when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); - when(repoMetaRepository.findByRepoName(anyString())).thenReturn(gitHubRepoMeta); + when(repoMetaRepo.findByRepoName(anyString())).thenReturn(gitHubRepoMeta); // Executes var result = productService.syncLatestDataFromMarketRepo(); @@ -426,16 +415,16 @@ void testNothingToSync() { void testSyncNullProductModuleContent() { var mockCommit = mockGHCommitHasSHA1(SHA1_SAMPLE); when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); - when(repoMetaRepository.findByRepoName(anyString())).thenReturn(null); + when(repoMetaRepo.findByRepoName(anyString())).thenReturn(null); Map> mockGHContentMap = new HashMap<>(); mockGHContentMap.put(SAMPLE_PRODUCT_ID, new ArrayList<>()); when(marketRepoService.fetchAllMarketItems()).thenReturn(mockGHContentMap); - when(productRepository.save(any(Product.class))).thenReturn(new Product()); + when(productRepo.save(any(Product.class))).thenReturn(new Product()); // Executes productService.syncLatestDataFromMarketRepo(); - verify(productRepository).save(argumentCaptor.capture()); + verify(productRepo).save(argumentCaptor.capture()); assertThat(argumentCaptor.getValue().getProductModuleContent()).isNull(); } @@ -446,66 +435,64 @@ void testSearchProducts() { String type = TypeOption.ALL.getOption(); keyword = "on"; language = "en"; - when(productRepository.searchByCriteria(any(), any(Pageable.class))).thenReturn( + when(productRepo.searchByCriteria(any(), any(Pageable.class))).thenReturn( mockResultReturn); var result = productService.findProducts(type, keyword, language, false, simplePageable); assertEquals(result, mockResultReturn); - verify(productRepository).searchByCriteria(any(), any(Pageable.class)); + verify(productRepo).searchByCriteria(any(), any(Pageable.class)); } @Test void testFetchProductDetail() { - String id = "amazon-comprehend"; - Product mockProduct = mockResultReturn.getContent().get(0); + MavenArtifactVersion mockMavenArtifactVersion = getMockMavenArtifactVersionWithData(); + Product mockProduct = getMockProduct(); + when(mavenArtifactVersionRepo.findById(MOCK_PRODUCT_ID)).thenReturn( + Optional.ofNullable(mockMavenArtifactVersion)); + when(productRepo.getProductByIdWithTagOrVersion(MOCK_PRODUCT_ID, MOCK_TAG_FROM_SNAPSHOT_VERSION)).thenReturn(null); mockProduct.setSynchronizedInstallationCount(true); - when(productRepository.getProductByIdWithNewestReleaseVersion(id, false)).thenReturn(mockProduct); - Product result = productService.fetchProductDetail(id, false); + Product result = productService.fetchProductDetail(MOCK_PRODUCT_ID, true); + assertNull(result); + } + + @Test + void testGetProductByIdWithNewestReleaseVersion() { + MavenArtifactVersion mockMavenArtifactVersion = getMockMavenArtifactVersionWithData(); + Product mockProduct = getMockProduct(); + when(mavenArtifactVersionRepo.findById(MOCK_PRODUCT_ID)).thenReturn( + Optional.ofNullable(mockMavenArtifactVersion)); + when(productRepo.getProductByIdWithTagOrVersion(MOCK_PRODUCT_ID, MOCK_TAG_FROM_SNAPSHOT_VERSION)).thenReturn(mockProduct); + Product result = productService.getProductByIdWithNewestReleaseVersion(MOCK_PRODUCT_ID, true); + assertEquals(mockProduct, result); + when(mavenArtifactVersionRepo.findById(MOCK_PRODUCT_ID)).thenReturn(Optional.ofNullable(null)); + when(productRepo.getReleasedVersionsById(MOCK_PRODUCT_ID)).thenReturn(List.of(MOCK_SNAPSHOT_VERSION)); + when(productRepo.getProductByIdWithTagOrVersion(MOCK_PRODUCT_ID, MOCK_SNAPSHOT_VERSION)).thenReturn(mockProduct); + result = productService.getProductByIdWithNewestReleaseVersion(MOCK_PRODUCT_ID, true); assertEquals(mockProduct, result); - verify(productRepository, times(1)).getProductByIdWithNewestReleaseVersion(id, false); } @Test void testFetchProductDetailByIdAndVersion() { - String id = "amazon-comprehend"; - String version = "10.0.2"; Product mockProduct = mockResultReturn.getContent().get(0); - when(productRepository.getProductByIdWithTagOrVersion(id, version)).thenReturn(mockProduct); + when(productRepo.getProductByIdWithTagOrVersion(MOCK_PRODUCT_ID, MOCK_RELEASED_VERSION)).thenReturn(mockProduct); - Product result = productService.fetchProductDetailByIdAndVersion(id, version); + Product result = productService.fetchProductDetailByIdAndVersion(MOCK_PRODUCT_ID, MOCK_RELEASED_VERSION); assertEquals(mockProduct, result); - verify(productRepository).getProductByIdWithTagOrVersion(id, version); + verify(productRepo).getProductByIdWithTagOrVersion(MOCK_PRODUCT_ID, MOCK_RELEASED_VERSION); } @Test void testFetchBestMatchProductDetailByIdAndVersion() { - String id = "amazon-comprehend"; - String version = "v10.0.2"; - String bestMatchVersion = "10.0.2"; - - MavenArtifactVersion mockMavenArtifactVersion = new MavenArtifactVersion(); - mockMavenArtifactVersion.getProductArtifactsByVersion().put(bestMatchVersion, Collections.emptyList()); - - List mockVersions = Arrays.asList("10.0.1", "10.0.2"); - when(mavenArtifactVersionRepo.findById(id)).thenReturn(Optional.of(mockMavenArtifactVersion)); - try (MockedStatic mockVersionUtils = Mockito.mockStatic(VersionUtils.class)) { - when(MavenUtils.getAllExistingVersions(mockMavenArtifactVersion, true, null)).thenReturn(mockVersions); - mockVersionUtils.when(() -> VersionUtils.getBestMatchVersion(mockVersions, version)).thenReturn(bestMatchVersion); - mockVersionUtils.when(() -> VersionUtils.convertVersionToTag(id, bestMatchVersion)).thenReturn(version); - - Product mockProduct = new Product(); - mockProduct.setSynchronizedInstallationCount(true); - when(productRepository.getProductByIdWithTagOrVersion(id, version)).thenReturn(mockProduct); - - Product result = productService.fetchBestMatchProductDetail(id, version); - - assertEquals(mockProduct, result); - assertEquals(bestMatchVersion, result.getBestMatchVersion()); - verify(mavenArtifactVersionRepo).findById(id); - verify(productRepository).getProductByIdWithTagOrVersion(id, version); - } + ReflectionTestUtils.setField(productService, LEGACY_INSTALLATION_COUNT_PATH_FIELD_NAME, INSTALLATION_FILE_PATH); + Product mockProduct = getMockProduct(); + Metadata mockMetadata = getMockMetadataWithVersions(); + mockMetadata.setArtifactId(MOCK_PRODUCT_ARTIFACT_ID); + when(metadataRepo.findByProductId(MOCK_PRODUCT_ID)).thenReturn(List.of(mockMetadata)); + when(productRepo.getProductByIdWithTagOrVersion(MOCK_PRODUCT_ID,MOCK_TAG_FROM_RELEASED_VERSION)).thenReturn(mockProduct); + Product result = productService.fetchBestMatchProductDetail(MOCK_PRODUCT_ID, MOCK_RELEASED_VERSION); + assertEquals(mockProduct, result); } @Test @@ -525,7 +512,7 @@ void testGetCompatibilityFromNumericTag() { void testRemoveFieldFromAllProductDocuments() { productService.removeFieldFromAllProductDocuments("customOrder"); - verify(mongoTemplate, times(1)).updateMulti(any(Query.class), any(Update.class), eq(Product.class)); + verify(mongoTemplate).updateMulti(any(Query.class), any(Update.class), eq(Product.class)); } @Test @@ -534,19 +521,19 @@ void testRefineOrderedListOfProductsInCustomSort() throws InvalidParamException List orderedListOfProducts = List.of(SAMPLE_PRODUCT_ID); Product mockProduct = new Product(); mockProduct.setId(SAMPLE_PRODUCT_ID); - when(productRepository.findById(SAMPLE_PRODUCT_ID)).thenReturn(Optional.of(mockProduct)); + when(productRepo.findById(SAMPLE_PRODUCT_ID)).thenReturn(Optional.of(mockProduct)); List refinedProducts = productService.refineOrderedListOfProductsInCustomSort(orderedListOfProducts); assertEquals(1, refinedProducts.size()); assertEquals(1, refinedProducts.get(0).getCustomOrder()); - verify(productRepository, times(1)).findById(SAMPLE_PRODUCT_ID); + verify(productRepo).findById(SAMPLE_PRODUCT_ID); } @Test void testRefineOrderedListOfProductsInCustomSort_ProductNotFound() { List orderedListOfProducts = List.of(SAMPLE_PRODUCT_ID); - when(productRepository.findById(SAMPLE_PRODUCT_ID)).thenReturn(Optional.empty()); + when(productRepo.findById(SAMPLE_PRODUCT_ID)).thenReturn(Optional.empty()); InvalidParamException exception = assertThrows(InvalidParamException.class, () -> productService.refineOrderedListOfProductsInCustomSort(orderedListOfProducts)); @@ -562,14 +549,14 @@ void testAddCustomSortProduct() throws InvalidParamException { Product mockProduct = new Product(); mockProduct.setId(SAMPLE_PRODUCT_ID); - when(productRepository.findById(SAMPLE_PRODUCT_ID)).thenReturn(Optional.of(mockProduct)); + when(productRepo.findById(SAMPLE_PRODUCT_ID)).thenReturn(Optional.of(mockProduct)); productService.addCustomSortProduct(customSortRequest); - verify(productCustomSortRepository).deleteAll(); + verify(productCustomSortRepo).deleteAll(); verify(mongoTemplate).updateMulti(any(Query.class), any(Update.class), eq(Product.class)); - verify(productCustomSortRepository).save(any(ProductCustomSort.class)); - verify(productRepository).saveAll(productListArgumentCaptor.capture()); + verify(productCustomSortRepo).save(any(ProductCustomSort.class)); + verify(productRepo).saveAll(productListArgumentCaptor.capture()); List capturedProducts = productListArgumentCaptor.getValue(); assertEquals(1, capturedProducts.size()); @@ -578,12 +565,12 @@ void testAddCustomSortProduct() throws InvalidParamException { @Test void testUpdateProductInstallationCountWhenNotSynchronized() { - Product product = mockProduct(); + Product product = getMockProduct(); product.setSynchronizedInstallationCount(false); String id = product.getId(); - ReflectionTestUtils.setField(productService, "legacyInstallationCountPath", INSTALLATION_FILE_PATH); + ReflectionTestUtils.setField(productService, LEGACY_INSTALLATION_COUNT_PATH_FIELD_NAME, INSTALLATION_FILE_PATH); - when(productRepository.updateInitialCount(eq(id), anyInt())).thenReturn(10); + when(productRepo.updateInitialCount(eq(id), anyInt())).thenReturn(10); productService.updateProductInstallationCount(id, product); @@ -602,8 +589,8 @@ void testCreateOrder() { void testClearAllProducts() { productService.clearAllProducts(); - verify(repoMetaRepository).deleteAll(); - verify(productRepository).deleteAll(); + verify(repoMetaRepo).deleteAll(); + verify(productRepo).deleteAll(); } private void mockMarketRepoMetaStatus() { @@ -612,7 +599,7 @@ private void mockMarketRepoMetaStatus() { mockMarketRepoMeta.setRepoName(GitHubConstants.AXONIVY_MARKETPLACE_REPO_NAME); mockMarketRepoMeta.setLastChange(LAST_CHANGE_TIME); mockMarketRepoMeta.setLastSHA1(SHA1_SAMPLE); - when(repoMetaRepository.findByRepoName(any())).thenReturn(mockMarketRepoMeta); + when(repoMetaRepo.findByRepoName(any())).thenReturn(mockMarketRepoMeta); } private GHCommit mockGHCommitHasSHA1(String sha1) { @@ -635,21 +622,15 @@ private GHContent mockGHContentAsLogo() { private ProductModuleContent mockReadmeProductContent() { ProductModuleContent productModuleContent = new ProductModuleContent(); - productModuleContent.setId("amazon-comprehendv-10.0.2"); - productModuleContent.setTag("v10.0.2"); - productModuleContent.setName("Amazon Comprehend"); + productModuleContent.setId(MOCK_PRODUCT_ID_WITH_TAG); + productModuleContent.setTag(MOCK_TAG_FROM_RELEASED_VERSION); + productModuleContent.setName(MOCK_PRODUCT_NAME); Map description = new HashMap<>(); description.put(Language.EN.getValue(), "testDescription"); productModuleContent.setDescription(description); return productModuleContent; } - private List mockProducts() { - Product product1 = Product.builder().id("amazon-comprehend-connector").repositoryName("axonivy-market/amazon-comprehend-connector") - .productModuleContent(mockReadmeProductContent()).build(); - return List.of(product1); - } - @Test void testUpdateNewLogoFromGitHub_removeOldLogo() throws IOException { // Start testing by adding new logo @@ -663,8 +644,6 @@ void testUpdateNewLogoFromGitHub_removeOldLogo() throws IOException { mockGitHubFile.setType(FileType.LOGO); mockGitHubFile.setStatus(FileStatus.ADDED); when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGitHubFile)); - var mockGHContent = mockGHContentAsLogo(); - when(gitHubService.getGHContent(any(), anyString(), any())).thenReturn(mockGHContent); // Executes var result = productService.syncLatestDataFromMarketRepo(); @@ -675,14 +654,14 @@ void testUpdateNewLogoFromGitHub_removeOldLogo() throws IOException { when(mockCommit.getSHA1()).thenReturn(UUID.randomUUID().toString()); mockGitHubFile.setStatus(FileStatus.REMOVED); when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGitHubFile)); - when(imageRepository.findByImageUrlEndsWithIgnoreCase(anyString())) + when(imageRepo.findByImageUrlEndsWithIgnoreCase(anyString())) .thenReturn(List.of(GHAxonIvyProductRepoServiceImplTest.mockImage())); // Executes result = productService.syncLatestDataFromMarketRepo(); - verify(productRepository, times(1)).deleteById(anyString()); - verify(imageRepository, times(1)).deleteAllByProductId(anyString()); - verify(imageRepository, times(1)).findByImageUrlEndsWithIgnoreCase(anyString()); + verify(productRepo).deleteById(anyString()); + verify(imageRepo).deleteAllByProductId(anyString()); + verify(imageRepo).findByImageUrlEndsWithIgnoreCase(anyString()); assertNotNull(result); assertFalse(result.isEmpty()); } @@ -700,14 +679,62 @@ void testUpdateNewLogoFromGitHub_ModifyLogo() throws IOException { mockGitHubFile.setType(FileType.META); mockGitHubFile.setStatus(FileStatus.REMOVED); when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGitHubFile)); - when(productRepository.findByMarketDirectory(anyString())).thenReturn(mockProducts()); + when(productRepo.findByMarketDirectory(anyString())).thenReturn(getMockProducts()); // Executes var result = productService.syncLatestDataFromMarketRepo(); assertNotNull(result); assertFalse(result.isEmpty()); - verify(productRepository, times(1)).deleteById(anyString()); - verify(imageRepository, times(1)).deleteAllByProductId(anyString()); + verify(productRepo).deleteById(anyString()); + verify(imageRepo).deleteAllByProductId(anyString()); + } + + @Test + void testSyncOneProduct() throws IOException { + Product mockProduct = new Product(); + mockProduct.setId(SAMPLE_PRODUCT_ID); + mockProduct.setMarketDirectory(SAMPLE_PRODUCT_PATH); + when(productRepo.findById(anyString())).thenReturn(Optional.of(mockProduct)); + var mockContents = mockMetaJsonAndLogoList(); + when(marketRepoService.getMarketItemByPath(anyString())).thenReturn(mockContents); + when(metadataService.syncProductMetadata(any(Product.class))).thenReturn(true); + when(productRepo.save(any(Product.class))).thenReturn(mockProduct); + // Executes + var result = productService.syncOneProduct(SAMPLE_PRODUCT_PATH, SAMPLE_PRODUCT_ID, false); + assertTrue(result); + } + + private List mockMetaJsonAndLogoList() throws IOException { + var mockContent = mockGHContentAsMetaJSON(); + InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); + when(mockContent.read()).thenReturn(inputStream); + + var mockContentLogo = mockGHContentAsLogo(); + return new ArrayList<>(List.of(mockContent, mockContentLogo)); + } + + @Test + void testSyncProductsAsUpdateMetaJSONFromGitHub_AddVendorLogo() throws IOException { + // Start testing by adding new meta + mockMarketRepoMetaStatus(); + var mockCommit = mockGHCommitHasSHA1(UUID.randomUUID().toString()); + when(mockCommit.getCommitDate()).thenReturn(new Date()); + when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); + + var mockGithubFile = new GitHubFile(); + mockGithubFile.setFileName(META_FILE); + mockGithubFile.setType(FileType.META); + mockGithubFile.setStatus(FileStatus.MODIFIED); + when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGithubFile)); + var mockGHContent = mockGHContentAsMetaJSON(); + when(gitHubService.getGHContent(any(), anyString(), any())).thenReturn(mockGHContent); + when(mockGHContent.read()).thenReturn(this.getClass().getResourceAsStream(META_JSON_FILE_WITH_VENDOR_INFORMATION)); + when(productRepo.save(any(Product.class))).thenReturn(new Product()); + + // Executes + var result = productService.syncLatestDataFromMarketRepo(); + assertNotNull(result); + assertTrue(result.isEmpty()); } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/VersionServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/VersionServiceImplTest.java index 9ea6bedb1..8bc7f8f5e 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/VersionServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/VersionServiceImplTest.java @@ -5,12 +5,15 @@ import com.axonivy.market.bo.Artifact; import com.axonivy.market.constants.MavenConstants; import com.axonivy.market.entity.MavenArtifactVersion; +import com.axonivy.market.entity.Metadata; import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductJsonContent; +import com.axonivy.market.enums.DevelopmentVersion; import com.axonivy.market.github.service.GHAxonIvyProductRepoService; import com.axonivy.market.model.MavenArtifactModel; import com.axonivy.market.model.VersionAndUrlModel; import com.axonivy.market.repository.MavenArtifactVersionRepository; +import com.axonivy.market.repository.MetadataRepository; import com.axonivy.market.repository.ProductJsonContentRepository; import com.axonivy.market.repository.ProductModuleContentRepository; import com.axonivy.market.repository.ProductRepository; @@ -30,6 +33,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -47,7 +51,7 @@ class VersionServiceImplTest extends BaseSetup { private GHAxonIvyProductRepoService gitHubService; @Mock - private MavenArtifactVersionRepository mavenArtifactVersionRepository; + private MavenArtifactVersionRepository mavenArtifactVersionRepo; @Mock private ProductRepository productRepository; @@ -58,10 +62,13 @@ class VersionServiceImplTest extends BaseSetup { @Mock private ProductModuleContentRepository productModuleContentRepository; + @Mock + private MetadataRepository metadataRepo; + @Test void testGetArtifactsAndVersionToDisplay() { - when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.empty()); - when(mavenArtifactVersionRepository.findById(MOCK_PRODUCT_ID)).thenReturn( + when(mavenArtifactVersionRepo.findById(Mockito.anyString())).thenReturn(Optional.empty()); + when(mavenArtifactVersionRepo.findById(MOCK_PRODUCT_ID)).thenReturn( Optional.ofNullable(MavenArtifactVersion.builder().productId(MOCK_PRODUCT_ID).productArtifactsByVersion( new HashMap<>()).additionalArtifactsByVersion(new HashMap<>()).build())); Assertions.assertTrue(CollectionUtils.isEmpty( @@ -75,7 +82,7 @@ void testGetArtifactsAndVersionToDisplay() { mockModel.setName(MOCK_PRODUCT_ID); mockModel.setDownloadUrl(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); proceededData.getAdditionalArtifactsByVersion().put(MOCK_RELEASED_VERSION, List.of(mockModel)); - when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.of(proceededData)); + when(mavenArtifactVersionRepo.findById(Mockito.anyString())).thenReturn(Optional.of(proceededData)); Assertions.assertTrue(ObjectUtils.isNotEmpty( versionService.getArtifactsAndVersionToDisplay(MOCK_PRODUCT_ID, false, MOCK_RELEASED_VERSION))); } @@ -125,11 +132,17 @@ void testFindArchivedArtifactInfoBestMatchWithVersion() { void testGetVersionsForDesigner() { MavenArtifactVersion mockMavenArtifactVersion = new MavenArtifactVersion(); List mockVersions = List.of("11.3.0-SNAPSHOT", "11.1.1", "11.1.0", "10.0.2"); + Metadata mockMetadata = getMockMetadata(); + mockMetadata.setArtifactId(MOCK_PRODUCT_ARTIFACT_ID); + mockMetadata.setVersions(new HashSet<>()); + mockMetadata.getVersions().addAll(mockVersions); for (String version : mockVersions) { mockMavenArtifactVersion.getProductArtifactsByVersion().put(version, new ArrayList<>()); } - when(mavenArtifactVersionRepository.findById(MOCK_PRODUCT_ID)).thenReturn(Optional.of(mockMavenArtifactVersion)); List result = versionService.getVersionsForDesigner(MOCK_PRODUCT_ID); + Assertions.assertTrue(CollectionUtils.isEmpty(result)); + when(metadataRepo.findByProductId(MOCK_PRODUCT_ID)).thenReturn(List.of(mockMetadata)); + result = versionService.getVersionsForDesigner(MOCK_PRODUCT_ID); Assertions.assertEquals(result.stream().map(VersionAndUrlModel::getVersion).toList(), mockVersions); Assertions.assertTrue(result.get(0).getUrl().endsWith("/api/product-details/bpmn-statistic/11.3.0-SNAPSHOT/json")); Assertions.assertTrue(result.get(1).getUrl().endsWith("/api/product-details/bpmn-statistic/11.1.1/json")); @@ -171,12 +184,40 @@ void testGetAllExistingVersions() { MavenArtifactVersion mockMavenArtifactVersion = new MavenArtifactVersion(); Assertions.assertTrue(CollectionUtils.isEmpty(MavenUtils.getAllExistingVersions(mockMavenArtifactVersion, false, StringUtils.EMPTY))); - Map> mockArtifactModelsByVersion = new HashMap<>(); - mockArtifactModelsByVersion.put(MOCK_SNAPSHOT_VERSION, new ArrayList<>()); - mockMavenArtifactVersion.setProductArtifactsByVersion(mockArtifactModelsByVersion); + mockMavenArtifactVersion = getMockMavenArtifactVersionWithData(); Assertions.assertTrue(ObjectUtils.isNotEmpty(MavenUtils.getAllExistingVersions(mockMavenArtifactVersion, true, StringUtils.EMPTY))); Assertions.assertTrue(CollectionUtils.isEmpty(MavenUtils.getAllExistingVersions(mockMavenArtifactVersion, false, StringUtils.EMPTY))); } + + @Test + void testGetDownloadUrlFromExistingDataByArtifactIdAndVersion() { + Map> existingData = getMockMavenArtifactVersion().getProductArtifactsByVersion(); + existingData.put(MOCK_RELEASED_VERSION, List.of(getMockMavenArtifactModelWithDownloadUrl())); + Assertions.assertNull(versionService.getDownloadUrlFromExistingDataByArtifactIdAndVersion(existingData, + MOCK_SNAPSHOT_VERSION, List.of(MOCK_ARTIFACT_ID))); + Assertions.assertEquals(MOCK_DOWNLOAD_URL,versionService.getDownloadUrlFromExistingDataByArtifactIdAndVersion(existingData, + MOCK_RELEASED_VERSION, List.of(MOCK_ARTIFACT_ID))); + } + + @Test + void testGetLatestVersionArtifactDownloadUrl() { + Assertions.assertEquals(StringUtils.EMPTY, versionService.getLatestVersionArtifactDownloadUrl(MOCK_PRODUCT_ID, + DevelopmentVersion.LATEST.getCode(), MOCK_ARTIFACT_DOWNLOAD_FILE)); + + when(metadataRepo.findByProductIdAndArtifactId(MOCK_PRODUCT_ID, MOCK_ARTIFACT_ID)).thenReturn(List.of(getMockMetadataWithVersions())); + Assertions.assertEquals(StringUtils.EMPTY, versionService.getLatestVersionArtifactDownloadUrl(MOCK_PRODUCT_ID, + DevelopmentVersion.LATEST.getCode(), MOCK_ARTIFACT_DOWNLOAD_FILE)); + + MavenArtifactVersion mockMavenArtifactVersion = getMockMavenArtifactVersion(); + when(mavenArtifactVersionRepo.findById(anyString())).thenReturn(Optional.ofNullable(mockMavenArtifactVersion)); + Assertions.assertEquals(StringUtils.EMPTY, versionService.getLatestVersionArtifactDownloadUrl(MOCK_PRODUCT_ID, + DevelopmentVersion.LATEST.getCode(), MOCK_ARTIFACT_DOWNLOAD_FILE)); + + mockMavenArtifactVersion.getProductArtifactsByVersion().put(MOCK_RELEASED_VERSION, + List.of(getMockMavenArtifactModelWithDownloadUrl())); + Assertions.assertEquals(MOCK_DOWNLOAD_URL, versionService.getLatestVersionArtifactDownloadUrl(MOCK_PRODUCT_ID, + DevelopmentVersion.LATEST.getCode(), MOCK_ARTIFACT_DOWNLOAD_FILE)); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/util/MavenUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/MavenUtilsTest.java index a88860b30..bbda9f5bf 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/util/MavenUtilsTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/util/MavenUtilsTest.java @@ -23,6 +23,8 @@ import java.util.List; import java.util.Set; +import static com.axonivy.market.constants.MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL; + class MavenUtilsTest extends BaseSetup { @Test @@ -86,6 +88,21 @@ void testBuildSnapshotMetadataFromVersionUrlFromArtifactInfo() { MOCK_ARTIFACT_ID, MOCK_SNAPSHOT_VERSION)); } + @Test + void testExtractMavenArtifactsFromContentStream() throws IOException { + InputStream mockInputStream = getMockInputStream(); + List result = MavenUtils.extractMavenArtifactsFromContentStream(mockInputStream); + for (Artifact artifact : result) { + Assertions.assertEquals(DEFAULT_IVY_MAVEN_BASE_URL, artifact.getRepoUrl()); + } + + mockInputStream = getMockProductJsonNodeContentInputStream(); + result = MavenUtils.extractMavenArtifactsFromContentStream(mockInputStream); + for (Artifact artifact : result) { + Assertions.assertEquals(DEFAULT_IVY_MAVEN_BASE_URL, artifact.getRepoUrl()); + } + } + @Test void testBuildMetadataUrlFromArtifactInfo() { Assertions.assertTrue(StringUtils.isEmpty(MavenUtils.buildMetadataUrlFromArtifactInfo(null, null, null))); @@ -206,7 +223,7 @@ void testFilterNonProductArtifactFromList() { @Test void testExtractedContentStream() throws IOException { - Assertions.assertNull(MavenUtils.extractedContentStream(Path.of(INAVALID_FILE_PATH))); + Assertions.assertNull(MavenUtils.extractedContentStream(Path.of(INVALID_FILE_PATH))); InputStream expectedResult = IOUtils.toInputStream(getMockSnapShotMetadataContent(), StandardCharsets.UTF_8); InputStream result = MavenUtils.extractedContentStream(Path.of(MOCK_SNAPSHOT_METADATA_FILE_PATH)); Assertions.assertNotNull(result); @@ -216,7 +233,7 @@ void testExtractedContentStream() throws IOException { @Test void testConvertProductJsonToMavenProductInfo() { try { - List result = MavenUtils.convertProductJsonToMavenProductInfo(Path.of(INAVALID_FILE_PATH)); + List result = MavenUtils.convertProductJsonToMavenProductInfo(Path.of(INVALID_FILE_PATH)); Assertions.assertTrue(CollectionUtils.isEmpty(result)); result = MavenUtils.convertProductJsonToMavenProductInfo(Path.of(MOCK_PRODUCT_JSON_FILE_PATH)); Assertions.assertTrue(CollectionUtils.isEmpty(result)); diff --git a/marketplace-service/src/test/resources/installationCount.json b/marketplace-service/src/test/resources/installationCount.json index a0c8a3fbb..19b687a4a 100644 --- a/marketplace-service/src/test/resources/installationCount.json +++ b/marketplace-service/src/test/resources/installationCount.json @@ -1,3 +1,4 @@ { - "google-maps-connector": 40 + "google-maps-connector": 40, + "bpmn-statistic": 40 } \ No newline at end of file diff --git a/marketplace-service/src/test/resources/meta-with-vendor-information.json b/marketplace-service/src/test/resources/meta-with-vendor-information.json new file mode 100644 index 000000000..c72b9b60c --- /dev/null +++ b/marketplace-service/src/test/resources/meta-with-vendor-information.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.axonivy.com/market/10.0.3/meta.json", + "id": "jira-connector", + "name": "Atlassian Jira", + "names": [ + { + "locale": "en", + "value": "Atlassian Jira" + }, + { + "locale": "de", + "value": "Atlassian Jira" + } + ], + "description": "Atlassian's Jira connector lets you track issues directly from the Axon Ivy platform.", + "descriptions": [ + { + "locale": "en", + "value": "Atlassian's Jira connector lets you track issues directly from the Axon Ivy platform." + }, + { + "locale": "de", + "value": "Nutze den Jira Connector von Atlassian, um Jira-Tickets direkt von der Axon Ivy Plattform aus zu managen." + } + ], + "type": "connector", + "platformReview": "4.5", + + "vendor": "FROX AG", + "vendorImage": "frox.png", + "vendorImageDarkMode": "frox-dark-mode.png", + "vendorUrl": "https://www.frox.ch", + + "sourceUrl": "https://github.com/axonivy-market/jira-connector", + "statusBadgeUrl": "https://github.com/axonivy-market/jira-connector/actions/workflows/ci.yml/badge.svg", + "language": "English", + "industry": "Cross-Industry", + "tags": ["helper"], + "mavenArtifacts": [ + { + "repoUrl": "https://maven.axonivy.com", + "name": "Jira Connector Product", + "groupId": "com.axonivy.connector.jira", + "artifactId": "jira-connector-product", + "type": "zip" + } + ] +} \ No newline at end of file diff --git a/marketplace-service/src/test/resources/productMissingURL.json b/marketplace-service/src/test/resources/productMissingURL.json new file mode 100644 index 000000000..a6d7e1671 --- /dev/null +++ b/marketplace-service/src/test/resources/productMissingURL.json @@ -0,0 +1,18 @@ +{ + "installers": [ + { + "id": "maven-dependency", + "data": { + "dependencies": [ + { + "groupId": "com.axonivy.ivy.webtest", + "artifactId": "web-tester", + "version": "${version}", + "type": "jar", + "scope": "test" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/marketplace-service/src/test/resources/setup.md b/marketplace-service/src/test/resources/setup.md new file mode 100644 index 000000000..46d23b7f0 --- /dev/null +++ b/marketplace-service/src/test/resources/setup.md @@ -0,0 +1,11 @@ +## Setup +### Variables +In order to use this product you must configure multiple variables. + +Add the following block to your `config/variables.yaml` file of ours +main Business Project that will make use of this product: + +``` +@variables.yaml@ +``` +![set-redirect](image.png) \ No newline at end of file diff --git a/marketplace-ui/angular.json b/marketplace-ui/angular.json index 815387bd7..2999e3a75 100644 --- a/marketplace-ui/angular.json +++ b/marketplace-ui/angular.json @@ -33,6 +33,7 @@ "node_modules/@fortawesome/fontawesome-free/css/all.min.css" ], "scripts": [ + "node_modules/emoji-toolkit/lib/js/joypixels.min.js", "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" ] }, @@ -121,7 +122,7 @@ "inlineStyleLanguage": "scss", "assets": ["src/assets"], "styles": ["src/styles.scss"], - "scripts": [], + "scripts": ["node_modules/emoji-toolkit/lib/js/joypixels.min.js"], "karmaConfig": "karma.conf.js", "sourceMap": true, "watch": false diff --git a/marketplace-ui/package-lock.json b/marketplace-ui/package-lock.json index d5ba90e32..033d6f16e 100644 --- a/marketplace-ui/package-lock.json +++ b/marketplace-ui/package-lock.json @@ -24,6 +24,7 @@ "@popperjs/core": "^2.11.8", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", + "emoji-toolkit": "^9.0.0", "jwt-decode": "^4.0.0", "karma-viewport": "^1.0.9", "marked": "^12.0.0", @@ -7625,10 +7626,9 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/emoji-toolkit": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-8.0.0.tgz", - "integrity": "sha512-Vz8YIqQJsQ+QZ4yuKMMzliXceayqfWbNjb6bST+vm77QAhU2is3I+/PRxrNknW+q1bvHHMgjLCQXxzINWLVapg==", - "optional": true + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-9.0.0.tgz", + "integrity": "sha512-59SKNdBZT3v8bMjkOkBaQqsDuKJK5F2AzRIlGAhAoHAVnhJ51EreKkTkoMAhIp2mOUQDP4CUWqmDV8GGWP1aMw==" }, "node_modules/emojis-list": { "version": "3.0.0", @@ -12144,6 +12144,12 @@ "zone.js": "~0.14.0" } }, + "node_modules/ngx-markdown/node_modules/emoji-toolkit": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-8.0.0.tgz", + "integrity": "sha512-Vz8YIqQJsQ+QZ4yuKMMzliXceayqfWbNjb6bST+vm77QAhU2is3I+/PRxrNknW+q1bvHHMgjLCQXxzINWLVapg==", + "optional": true + }, "node_modules/ngxtension": { "version": "3.5.5", "resolved": "https://registry.npmjs.org/ngxtension/-/ngxtension-3.5.5.tgz", diff --git a/marketplace-ui/package.json b/marketplace-ui/package.json index 778fa435b..c3b5f3f16 100644 --- a/marketplace-ui/package.json +++ b/marketplace-ui/package.json @@ -26,11 +26,12 @@ "@popperjs/core": "^2.11.8", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", + "emoji-toolkit": "^9.0.0", "jwt-decode": "^4.0.0", "karma-viewport": "^1.0.9", "marked": "^12.0.0", - "ngx-markdown": "^18.0.0", "ngx-cookie-service": "^18.0.0", + "ngx-markdown": "^18.0.0", "ngxtension": "^3.5.5", "rxjs": "~7.8.0", "tslib": "^2.3.0", diff --git a/marketplace-ui/src/app/app.component.scss b/marketplace-ui/src/app/app.component.scss index dba00e98b..693e97949 100644 --- a/marketplace-ui/src/app/app.component.scss +++ b/marketplace-ui/src/app/app.component.scss @@ -32,7 +32,7 @@ footer { top: 0; left: 0; width: 100vw; - padding-bottom: 3rem; + padding-bottom: 7rem; } .header-mobile-container { diff --git a/marketplace-ui/src/app/app.routes.ts b/marketplace-ui/src/app/app.routes.ts index 919558dba..4fc2fc5a8 100644 --- a/marketplace-ui/src/app/app.routes.ts +++ b/marketplace-ui/src/app/app.routes.ts @@ -1,7 +1,7 @@ import { Routes } from '@angular/router'; import { GithubCallbackComponent } from './auth/github-callback/github-callback.component'; import { ErrorPageComponent } from './shared/components/error-page/error-page.component'; -import { ExternalDocumentComponent } from './shared/components/external-document/external-document.component'; +import { RedirectPageComponent } from './shared/components/redirect-page/redirect-page.component'; export const routes: Routes = [ { @@ -23,7 +23,11 @@ export const routes: Routes = [ }, { path: ':id/:version/doc', - component: ExternalDocumentComponent + component: RedirectPageComponent + }, + { + path: ':id/:version/lib/:artifact', + component: RedirectPageComponent }, { path: 'auth/github/callback', diff --git a/marketplace-ui/src/app/core/interceptors/api.interceptor.ts b/marketplace-ui/src/app/core/interceptors/api.interceptor.ts index 91b9b729c..a9447f370 100644 --- a/marketplace-ui/src/app/core/interceptors/api.interceptor.ts +++ b/marketplace-ui/src/app/core/interceptors/api.interceptor.ts @@ -13,11 +13,16 @@ import { ERROR_CODES, ERROR_PAGE_PATH } from '../../shared/constants/common.cons export const REQUEST_BY = 'X-Requested-By'; export const IVY = 'marketplace-website'; -/** This is option for exclude loading api +/** SkipLoading: This option for exclude loading api * @Example return httpClient.get('apiEndPoint', { context: new HttpContext().set(SkipLoading, true) }) */ export const SkipLoading = new HttpContextToken(() => false); +/** ForwardingError: This option for forwarding responce error to the caller + * @Example return httpClient.get('apiEndPoint', { context: new HttpContext().set(ForwardingError, true) }) + */ +export const ForwardingError = new HttpContextToken(() => false); + export const apiInterceptor: HttpInterceptorFn = (req, next) => { const router = inject(Router); const loadingService = inject(LoadingService); @@ -36,11 +41,13 @@ export const apiInterceptor: HttpInterceptorFn = (req, next) => { headers: addIvyHeaders(req.headers) }); - if (req.context.get(SkipLoading)) { - return next(cloneReq); + if (!req.context.get(SkipLoading)) { + loadingService.show(); } - loadingService.show(); + if (req.context.get(ForwardingError)) { + return next(cloneReq); + } return next(cloneReq).pipe( catchError(error => { @@ -52,7 +59,9 @@ export const apiInterceptor: HttpInterceptorFn = (req, next) => { return EMPTY; }), finalize(() => { - loadingService.hide(); + if (!req.context.get(SkipLoading)) { + loadingService.hide(); + } }) ); }; diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.spec.ts index c055923be..ed33d05d4 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.spec.ts @@ -10,12 +10,21 @@ import { ItemDropdown } from '../../../../../../shared/models/item-dropdown.mode import { TypeOption } from '../../../../../../shared/enums/type-option.enum'; import { SortOption } from '../../../../../../shared/enums/sort-option.enum'; import { FeedbackSortType } from '../../../../../../shared/enums/feedback-sort-type'; +import { FeedbackFilterService } from './feedback-filter.service'; +import { Subject } from 'rxjs'; describe('FeedbackFilterComponent', () => { + const mockEvent = { + value: FeedbackSortType.NEWEST, + label: 'common.sort.value.newest', + sortFn: 'updatedAt,desc' + } as ItemDropdown; + let component: FeedbackFilterComponent; let fixture: ComponentFixture; let translateService: jasmine.SpyObj; let productFeedbackService: jasmine.SpyObj; + let feedbackFilterService: FeedbackFilterService; beforeEach(async () => { const productFeedbackServiceSpy = jasmine.createSpyObj('ProductFeedbackService', ['sort']); @@ -23,6 +32,14 @@ describe('FeedbackFilterComponent', () => { await TestBed.configureTestingModule({ imports: [FeedbackFilterComponent, FormsModule, TranslateModule.forRoot() ], providers: [ + { + provide: FeedbackFilterService, + useValue: { + event$: new Subject(), + data: null, + changeSortByLabel: jasmine.createSpy('changeSortByLabel') + } + }, TranslateService, { provide: ProductFeedbackService, useValue: productFeedbackServiceSpy } ] @@ -31,6 +48,7 @@ describe('FeedbackFilterComponent', () => { translateService = TestBed.inject(TranslateService) as jasmine.SpyObj; productFeedbackService = TestBed.inject(ProductFeedbackService) as jasmine.SpyObj; + feedbackFilterService = TestBed.inject(FeedbackFilterService); }); beforeEach(() => { @@ -69,4 +87,31 @@ describe('FeedbackFilterComponent', () => { const dropdownComponent = fixture.debugElement.query(By.directive(CommonDropdownComponent)).componentInstance; expect(dropdownComponent.items).toBe(component.feedbackSortTypes); }); + + it('should emit sortChange event when onSortChange is called', () => { + spyOn(component.sortChange, 'emit'); + component.onSortChange(mockEvent); + expect(component.sortChange.emit).toHaveBeenCalledWith(mockEvent.sortFn); + }); + + it('should listen to feedbackFilterService event$ and call changeSortByLabel', () => { + spyOn(component, 'changeSortByLabel').and.callThrough(); + component.ngOnInit(); // Subscribes to event$ + (feedbackFilterService.event$ as Subject).next(mockEvent); // Trigger the event + expect(component.changeSortByLabel).toHaveBeenCalledWith(mockEvent); + }); + + it('should NOT call changeSortByLabel if feedbackFilterService.data does not exist', () => { + feedbackFilterService.data = undefined; + spyOn(component, 'changeSortByLabel'); + component.ngOnInit() + expect(component.changeSortByLabel).not.toHaveBeenCalled(); + }); + + it('should call changeSortByLabel if feedbackFilterService.data exists', () => { + feedbackFilterService.data = mockEvent; + spyOn(component, 'changeSortByLabel'); + component.ngOnInit() + expect(component.changeSortByLabel).toHaveBeenCalledWith(mockEvent); + }); }); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.ts index 91338569f..bf62bf80f 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, inject, Output } from '@angular/core'; +import { Component, EventEmitter, inject, OnInit, Output } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { FEEDBACK_SORT_TYPES } from '../../../../../../shared/constants/common.constant'; import { FormsModule } from '@angular/forms'; @@ -8,6 +8,7 @@ import { CommonDropdownComponent } from '../../../../../../shared/components/com import { CommonUtils } from '../../../../../../shared/utils/common.utils'; import { ItemDropdown } from '../../../../../../shared/models/item-dropdown.model'; import { FeedbackSortType } from '../../../../../../shared/enums/feedback-sort-type'; +import { FeedbackFilterService } from './feedback-filter.service'; @Component({ selector: 'app-feedback-filter', @@ -16,18 +17,36 @@ import { FeedbackSortType } from '../../../../../../shared/enums/feedback-sort-t templateUrl: './feedback-filter.component.html', styleUrl: './feedback-filter.component.scss' }) -export class FeedbackFilterComponent { +export class FeedbackFilterComponent implements OnInit { feedbackSortTypes = FEEDBACK_SORT_TYPES; @Output() sortChange = new EventEmitter(); + feedbackFilterService = inject(FeedbackFilterService); + productFeedbackService = inject(ProductFeedbackService); languageService = inject(LanguageService); selectedSortTypeLabel: string = CommonUtils.getLabel(FEEDBACK_SORT_TYPES[0].value, FEEDBACK_SORT_TYPES); + ngOnInit() { + if (this.feedbackFilterService.data) { + this.changeSortByLabel(this.feedbackFilterService.data); + } + this.feedbackFilterService.event$.subscribe(event => { + this.changeSortByLabel(event); + }); + } + onSortChange(event: ItemDropdown): void { - this.selectedSortTypeLabel = CommonUtils.getLabel(event.value, FEEDBACK_SORT_TYPES); + this.changeSortByLabel(event); this.sortChange.emit(event.sortFn); + this.feedbackFilterService.changeSortByLabel(event); } + changeSortByLabel(event: ItemDropdown): void { + this.selectedSortTypeLabel = CommonUtils.getLabel( + event.value, + FEEDBACK_SORT_TYPES + ); + } } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.service.spec.ts.js b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.service.spec.ts.js new file mode 100644 index 000000000..067c84334 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.service.spec.ts.js @@ -0,0 +1,42 @@ +import { TestBed } from '@angular/core/testing'; +import { FeedbackFilterService } from './feedback-filter.service'; + +describe('FeedbackFilterService', () => { + let service: FeedbackFilterService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(FeedbackFilterService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should have undefined data initially', () => { + expect(service.data).toBeUndefined(); + }); + + it('should update data and emit the value through event$', () => { + const mockData = { value: 'test', label: 'Test Label' }; + + // Spy on the next method of the Subject (sortBySubject) + spyOn(service['sortBySubject'], 'next').and.callThrough(); + + // Subscribe to the event$ observable to listen for changes + let emittedValue: any; + service.event$.subscribe(value => emittedValue = value); + + // Call the changeSortByLabel function + service.changeSortByLabel(mockData); + + // Expect the data to be updated + expect(service.data).toEqual(mockData); + + // Expect the next method to have been called with the correct value + expect(service['sortBySubject'].next).toHaveBeenCalledWith(mockData); + + // Expect the emitted value to match the mockData + expect(emittedValue).toEqual(mockData); + }); +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.service.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.service.ts new file mode 100644 index 000000000..31dfbfd28 --- /dev/null +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs'; +import { ItemDropdown } from '../../../../../../shared/models/item-dropdown.model'; +import { FeedbackSortType } from '../../../../../../shared/enums/feedback-sort-type'; + +@Injectable({ + providedIn: 'root' +}) +export class FeedbackFilterService { + private readonly sortBySubject = new Subject>(); + data: ItemDropdown | undefined; + event$ = this.sortBySubject.asObservable(); + + changeSortByLabel(data: ItemDropdown) { + this.data = data; + this.sortBySubject.next(data); + } +} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts index 258f434cf..87ee61820 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts @@ -93,14 +93,17 @@ describe('ProductFeedbackService', () => { service.loadMoreFeedbacks(); const loadMoreReq = httpMock.expectOne('api/feedback/product/123?page=1&size=8&sort=updatedAt,desc'); - loadMoreReq.flush({ _embedded: { feedbacks: additionalFeedback } }); - + loadMoreReq.flush({ _embedded: { feedbacks: additionalFeedback }, page: { totalPages: 2, totalElements: 5 } }); + expect(service.feedbacks()).toEqual([...initialFeedback, ...additionalFeedback]); }); it('should change sort and fetch feedbacks', () => { const mockResponse = { - _embedded: { feedbacks: [{ content: 'Sorting test', rating: 3, productId: '123' }] } + _embedded: { + feedbacks: [{ content: 'Sorting test', rating: 3, productId: '123' }] + }, + page: { totalPages: 2, totalElements: 5 } }; productDetailService.productId.and.returnValue('123'); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts index 3236febe2..f96fe48b6 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts @@ -11,7 +11,7 @@ import { signal, WritableSignal } from '@angular/core'; -import { catchError, Observable, of, tap } from 'rxjs'; +import { BehaviorSubject, catchError, concatMap, EMPTY, Observable, of, tap } from 'rxjs'; import { AuthService } from '../../../../../auth/auth.service'; import { SkipLoading } from '../../../../../core/interceptors/api.interceptor'; import { FeedbackApiResponse } from '../../../../../shared/models/apis/feedback-response.model'; @@ -29,6 +29,12 @@ export class ProductFeedbackService { private readonly productDetailService = inject(ProductDetailService); private readonly productStarRatingService = inject(ProductStarRatingService); private readonly http = inject(HttpClient); + private readonly feedbackRequestQueue$ = new BehaviorSubject<{ + productId: string; + page: number; + sort: string; + size: number; + } | null>(null); sort: WritableSignal = signal('updatedAt,desc'); page: WritableSignal = signal(0); @@ -45,6 +51,25 @@ export class ProductFeedbackService { totalPages: WritableSignal = signal(1); totalElements: WritableSignal = signal(0); + constructor() { + this.feedbackRequestQueue$ + .pipe( + concatMap(requestParams => { + if (requestParams) { + return this.executeFindProductFeedbacksByCriteria( + requestParams.productId, + requestParams.page, + requestParams.sort, + requestParams.size + ); + } else { + return EMPTY; + } + }) + ) + .subscribe(); + } + submitFeedback(feedback: Feedback): Observable { const headers = new HttpHeaders().set( 'X-Authorization', @@ -69,6 +94,15 @@ export class ProductFeedbackService { page: number = this.page(), sort: string = this.sort(), size: number = SIZE + ): void { + this.feedbackRequestQueue$.next({ productId, page, sort, size }); + } + + private executeFindProductFeedbacksByCriteria( + productId: string = this.productDetailService.productId(), + page: number = this.page(), + sort: string = this.sort(), + size: number = SIZE ): Observable { const requestParams = new HttpParams() .set('page', page.toString()) @@ -82,14 +116,9 @@ export class ProductFeedbackService { }) .pipe( tap(response => { - if (page === 0) { - this.feedbacks.set(response._embedded.feedbacks); - } else { - this.feedbacks.set([ - ...this.feedbacks(), - ...response._embedded.feedbacks - ]); - } + this.totalPages.set(response.page.totalPages); + this.totalElements.set(response.page.totalElements); + this.feedbacks.set([...this.feedbacks(), ...response._embedded.feedbacks]); }) ); } @@ -100,10 +129,8 @@ export class ProductFeedbackService { const params = new HttpParams() .set('productId', productId) .set('userId', this.authService.getUserId() ?? ''); - const requestURL = FEEDBACK_API_URL; - return this.http - .get(requestURL, { + .get(FEEDBACK_API_URL, { params, context: new HttpContext().set(SkipLoading, true) }) @@ -125,20 +152,18 @@ export class ProductFeedbackService { initFeedbacks(): void { this.page.set(0); - this.findProductFeedbacksByCriteria().subscribe(response => { - this.totalPages.set(response.page.totalPages); - this.totalElements.set(response.page.totalElements); - }); + this.findProductFeedbacksByCriteria(); } loadMoreFeedbacks(): void { this.page.update(value => value + 1); - this.findProductFeedbacksByCriteria().subscribe(); + this.findProductFeedbacksByCriteria(); } changeSort(newSort: string): void { + this.feedbacks.set([]); this.page.set(0); this.sort.set(newSort); - this.findProductFeedbacksByCriteria().subscribe(); + this.findProductFeedbacksByCriteria(); } } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html index 374a868b9..57a78e8d1 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html @@ -1,12 +1,21 @@ -

+

{{ 'common.product.detail.information.label' | translate }}

-
+
- {{ 'common.product.detail.information.value.author' | translate }} + {{ 'common.product.detail.information.value.implementedBy' | translate }} - {{ productDetail.vendor }} + + Logo Vendor +

@@ -14,7 +23,7 @@

- {{ selectedVersion.replaceAll('Version ', '') }} + {{ displayVersion }}

@if(productDetail.compatibility) { @@ -42,6 +51,17 @@

{{ productDetail.language }}

+ @if (externalDocumentLink !== '') { +
+
+ + {{ 'common.product.detail.information.value.documentation' | translate }} + + + {{ displayExternalDocName ?? 'common.product.detail.information.value.defaultDocName' | translate }} + +
+ }
@@ -95,4 +115,4 @@
} - @case ('customSolution') { + @case (ProductDetailActionType.CUSTOM_SOLUTION) {
@@ -165,7 +166,7 @@

[selectedVersion]="selectedVersion"> } @else { - } } - } + }
= signal( {} as ProductModuleContent ); + protected ProductDetailActionType = ProductDetailActionType; productDetailActionType = signal(ProductDetailActionType.STANDARD); detailTabs = PRODUCT_DETAIL_TABS; activeTab = ''; @@ -190,13 +193,18 @@ export class ProductDetailComponent { getProductById(productId: string, isShowDevVersion: boolean): Observable { const targetVersion = this.routingQueryParamService.getDesignerVersionFromCookie(); + let productDetail$: Observable; if (!targetVersion) { - return this.productService.getProductDetails(productId, isShowDevVersion); + productDetail$ = this.productService.getProductDetails(productId, isShowDevVersion); } - - return this.productService.getBestMatchProductDetailsWithVersion( - productId, - targetVersion + else { + productDetail$ = this.productService.getBestMatchProductDetailsWithVersion( + productId, + targetVersion + ); + } + return productDetail$.pipe( + map((response: ProductDetail) => this.setDefaultVendorImage(response)) ); } @@ -380,4 +388,19 @@ export class ProductDetailComponent { const value = key.value as tabName; return this.productModuleContent()[value]; } -} + + private setDefaultVendorImage(productDetail: ProductDetail): ProductDetail { + const { vendorImage, vendorImageDarkMode } = productDetail; + + if (!(productDetail.vendorImage || productDetail.vendorImageDarkMode )) { + productDetail.vendorImage = DEFAULT_VENDOR_IMAGE_BLACK; + productDetail.vendorImageDarkMode = DEFAULT_VENDOR_IMAGE; + } + else { + productDetail.vendorImage = vendorImage || vendorImageDarkMode; + productDetail.vendorImageDarkMode = vendorImageDarkMode || vendorImage; + } + + return productDetail; + } +} \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.service.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail.service.spec.ts index 70a8b44da..aae29c669 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail.service.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.service.spec.ts @@ -1,15 +1,24 @@ import { TestBed } from '@angular/core/testing'; import { ProductDetailService } from './product-detail.service'; import { DisplayValue } from '../../../shared/models/display-value.model'; +import { HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; describe('ProductDetailService', () => { let service: ProductDetailService; + let httpMock: HttpTestingController; + let httpClient: jasmine.SpyObj; beforeEach(() => { TestBed.configureTestingModule({ - providers: [ProductDetailService] + providers: [ProductDetailService, + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: HttpClient, useValue: httpClient } + ] }); service = TestBed.inject(ProductDetailService); + httpMock = TestBed.inject(HttpTestingController); }); it('should be created', () => { diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.service.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail.service.ts index 8272d25d2..ccbe32af4 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail.service.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.service.ts @@ -1,5 +1,11 @@ -import { Injectable, signal, WritableSignal } from '@angular/core'; +import { inject, Injectable, signal, WritableSignal } from '@angular/core'; import { DisplayValue } from '../../../shared/models/display-value.model'; +import { HttpClient, HttpContext } from '@angular/common/http'; +import { LoadingService } from '../../../core/services/loading/loading.service'; +import { Observable } from 'rxjs'; +import { API_URI } from '../../../shared/constants/api.constant'; +import { ForwardingError } from '../../../core/interceptors/api.interceptor'; +import { ExternalDocument } from '../../../shared/models/external-document.model'; @Injectable({ providedIn: 'root' @@ -7,4 +13,13 @@ import { DisplayValue } from '../../../shared/models/display-value.model'; export class ProductDetailService { productId: WritableSignal = signal(''); productNames: WritableSignal = signal({} as DisplayValue); + httpClient = inject(HttpClient); + loadingService = inject(LoadingService); + + + getExteralDocumentForProductByVersion(productId: string, version: string): Observable { + return this.httpClient.get( + `${API_URI.EXTERNAL_DOCUMENT}/${productId}/${version}`, { context: new HttpContext().set(ForwardingError, true)} + ); + } } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.scss index 4f230a2d5..3c00d204f 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.scss +++ b/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.scss @@ -16,7 +16,7 @@ } .analysis-title { - font-size: 1rem; + font-size: 1.6rem; } .product-analysis { @@ -24,6 +24,6 @@ } .analysis-title { - font-size: 1rem; + font-size: 1.6rem; } } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.scss index 10b3f402d..9bcc6d836 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.scss +++ b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.scss @@ -28,7 +28,7 @@ .review-label-detail-page { margin-bottom: 6px; - font-size: 1rem; + font-size: 1.6rem; } .message-star-rating-img { diff --git a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html index bc9125742..c4f8c31ed 100644 --- a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html +++ b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html @@ -50,7 +50,7 @@