From e1a0d8514395af868f6a3fe6061b735599750759 Mon Sep 17 00:00:00 2001 From: quanpham-axonivy Date: Tue, 17 Dec 2024 17:05:17 +0700 Subject: [PATCH] MARP-1548 Suspicious installation counts (#257) --- marketplace-build/.env | 3 +- marketplace-build/dev/.env | 3 +- marketplace-build/dev/docker-compose.yml | 2 + marketplace-build/docker-compose.yml | 2 + marketplace-build/release/.env | 3 +- marketplace-build/release/docker-compose.yml | 2 + marketplace-service/pom.xml | 6 +- .../market/constants/CommonConstants.java | 1 + .../market/constants/LoggingConstants.java | 22 +++++ .../ProductMarketplaceDataController.java | 16 ++-- .../com/axonivy/market/logging/Loggable.java | 11 +++ .../market/logging/LoggableAspect.java | 92 +++++++++++++++++++ .../com/axonivy/market/util/FileUtils.java | 31 +++++++ .../com/axonivy/market/util/LoggingUtils.java | 54 +++++++++++ .../src/main/resources/application.properties | 3 +- .../market/logging/LoggableAspectTest.java | 89 ++++++++++++++++++ .../service/impl/SchedulingTasksTest.java | 3 +- .../axonivy/market/util/FileUtilsTest.java | 57 ++++++++++++ .../axonivy/market/util/LoggingUtilsTest.java | 78 ++++++++++++++++ 19 files changed, 463 insertions(+), 15 deletions(-) create mode 100644 marketplace-service/src/main/java/com/axonivy/market/constants/LoggingConstants.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/logging/Loggable.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/logging/LoggableAspect.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/util/FileUtils.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/util/LoggingUtils.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/logging/LoggableAspectTest.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/util/FileUtilsTest.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/util/LoggingUtilsTest.java diff --git a/marketplace-build/.env b/marketplace-build/.env index d1b4b7edb..606142801 100644 --- a/marketplace-build/.env +++ b/marketplace-build/.env @@ -12,4 +12,5 @@ MARKET_GITHUB_OAUTH_APP_CLIENT_ID= MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET= MARKET_JWT_SECRET_KEY= MARKET_CORS_ALLOWED_ORIGIN=* -MARKET_MONGO_LOG_LEVEL=DEBUG \ No newline at end of file +MARKET_MONGO_LOG_LEVEL=DEBUG +MARKET_LOG_PATH=logs \ No newline at end of file diff --git a/marketplace-build/dev/.env b/marketplace-build/dev/.env index e9d068e53..09f4215ad 100644 --- a/marketplace-build/dev/.env +++ b/marketplace-build/dev/.env @@ -12,4 +12,5 @@ MARKET_GITHUB_OAUTH_APP_CLIENT_ID= MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET= MARKET_JWT_SECRET_KEY= MARKET_CORS_ALLOWED_ORIGIN=* -MARKET_MONGO_LOG_LEVEL=DEBUG \ No newline at end of file +MARKET_MONGO_LOG_LEVEL=DEBUG +MARKET_LOG_PATH=logs \ No newline at end of file diff --git a/marketplace-build/dev/docker-compose.yml b/marketplace-build/dev/docker-compose.yml index 260fd1f6c..ae81c5613 100644 --- a/marketplace-build/dev/docker-compose.yml +++ b/marketplace-build/dev/docker-compose.yml @@ -24,6 +24,7 @@ services: volumes: - /home/axonivy/marketplace/data/market-installations.json:/app/data/market-installation.json - marketcache:/app/data/market-cache + - ./logs:/app/logs environment: - MONGODB_HOST=${SERVICE_MONGODB_HOST} - MONGODB_DATABASE=${SERVICE_MONGODB_DATABASE} @@ -36,6 +37,7 @@ services: - MARKET_JWT_SECRET_KEY=${MARKET_JWT_SECRET_KEY} - MARKET_CORS_ALLOWED_ORIGIN=${MARKET_CORS_ALLOWED_ORIGIN} - MARKET_MONGO_LOG_LEVEL=${MARKET_MONGO_LOG_LEVEL} + - MARKET_LOG_PATH=${MARKET_LOG_PATH} build: context: ../../marketplace-service dockerfile: Dockerfile diff --git a/marketplace-build/docker-compose.yml b/marketplace-build/docker-compose.yml index 74011c3a5..d57c8ad8e 100644 --- a/marketplace-build/docker-compose.yml +++ b/marketplace-build/docker-compose.yml @@ -24,6 +24,7 @@ services: volumes: - /home/axonivy/marketplace/data/market-installations.json:/app/data/market-installation.json - marketcache:/app/data/market-cache + - ./logs:/app/logs environment: - MONGODB_HOST=${SERVICE_MONGODB_HOST} - MONGODB_DATABASE=${SERVICE_MONGODB_DATABASE} @@ -36,6 +37,7 @@ services: - MARKET_JWT_SECRET_KEY=${MARKET_JWT_SECRET_KEY} - MARKET_CORS_ALLOWED_ORIGIN=${MARKET_CORS_ALLOWED_ORIGIN} - MARKET_MONGO_LOG_LEVEL=${MARKET_MONGO_LOG_LEVEL} + - MARKET_LOG_PATH=${MARKET_LOG_PATH} build: context: ../marketplace-service dockerfile: Dockerfile diff --git a/marketplace-build/release/.env b/marketplace-build/release/.env index d8dfb5f8a..fd7ac7016 100644 --- a/marketplace-build/release/.env +++ b/marketplace-build/release/.env @@ -13,4 +13,5 @@ MARKET_GITHUB_OAUTH_APP_CLIENT_ID= MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET= MARKET_JWT_SECRET_KEY= MARKET_CORS_ALLOWED_ORIGIN=* -MARKET_MONGO_LOG_LEVEL=DEBUG \ No newline at end of file +MARKET_MONGO_LOG_LEVEL=DEBUG +MARKET_LOG_PATH=logs \ No newline at end of file diff --git a/marketplace-build/release/docker-compose.yml b/marketplace-build/release/docker-compose.yml index f8fc73f25..018614b70 100644 --- a/marketplace-build/release/docker-compose.yml +++ b/marketplace-build/release/docker-compose.yml @@ -22,6 +22,7 @@ services: volumes: - /home/axonivy/marketplace/data/market-installations.json:/app/data/market-installation.json - marketcache:/app/data/market-cache + - ./logs:/app/logs environment: - MONGODB_HOST=${SERVICE_MONGODB_HOST} - MONGODB_DATABASE=${SERVICE_MONGODB_DATABASE} @@ -34,6 +35,7 @@ services: - MARKET_JWT_SECRET_KEY=${MARKET_JWT_SECRET_KEY} - MARKET_CORS_ALLOWED_ORIGIN=${MARKET_CORS_ALLOWED_ORIGIN} - MARKET_MONGO_LOG_LEVEL=${MARKET_MONGO_LOG_LEVEL} + - MARKET_LOG_PATH=${MARKET_LOG_PATH} networks: - marketplace-network diff --git a/marketplace-service/pom.xml b/marketplace-service/pom.xml index 8bdb6e625..ec7ccc0ed 100644 --- a/marketplace-service/pom.xml +++ b/marketplace-service/pom.xml @@ -73,7 +73,11 @@ jaxb-api 2.3.1 - + + org.springframework.boot + spring-boot-starter-aop + + org.springframework.boot spring-boot-starter-validation diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java index 2966383f4..e80c2b57d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java @@ -6,6 +6,7 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class CommonConstants { public static final String REQUESTED_BY = "X-Requested-By"; + public static final String USER_AGENT = "user-agent"; public static final String SLASH = "/"; public static final String DOT_SEPARATOR = "."; public static final String PLUS = "+"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/LoggingConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/LoggingConstants.java new file mode 100644 index 000000000..2ce721583 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/LoggingConstants.java @@ -0,0 +1,22 @@ +package com.axonivy.market.constants; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class LoggingConstants { + + public static final String ENTRY_FORMAT = " <%s>%s%n"; + public static final String ENTRY_START = " \n"; + public static final String ENTRY_END = " \n"; + public static final String DATE_FORMAT = "yyyy-MM-dd"; + public static final String TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss"; + public static final String LOG_START = "\n"; + public static final String LOG_END = ""; + public static final String METHOD = "method"; + public static final String ARGUMENTS = "arguments"; + public static final String TIMESTAMP = "timestamp"; + public static final String NO_ARGUMENTS = "No arguments"; + public static final String MARKET_WEBSITE = "marketplace-website"; + +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductMarketplaceDataController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductMarketplaceDataController.java index 728251b33..cd4abe584 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductMarketplaceDataController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductMarketplaceDataController.java @@ -3,13 +3,12 @@ import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.enums.ErrorCode; import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.logging.Loggable; import com.axonivy.market.model.Message; import com.axonivy.market.model.ProductCustomSortRequest; import com.axonivy.market.service.ProductMarketplaceDataService; import com.axonivy.market.util.AuthorizationUtils; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.AllArgsConstructor; @@ -36,7 +35,7 @@ public class ProductMarketplaceDataController { private final GitHubService gitHubService; private final ProductMarketplaceDataService productMarketplaceDataService; - + @PostMapping(CUSTOM_SORT) @Operation(hidden = true) public ResponseEntity createCustomSortProducts( @@ -51,15 +50,14 @@ public ResponseEntity createCustomSortProducts( return new ResponseEntity<>(message, HttpStatus.OK); } + @Loggable + @Operation(hidden = true) @PutMapping(INSTALLATION_COUNT_BY_ID) - @Operation(summary = "Update installation count of product", - description = "By default, increase installation count when click download product files by users") public ResponseEntity syncInstallationCount( - @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "approval-decision-utils", - in = ParameterIn.PATH) String productId, - @RequestParam(name = DESIGNER_VERSION, required = false) @Parameter(in = ParameterIn.QUERY, - example = "v10.0.20") String designerVersion) { + @PathVariable(ID) String productId, + @RequestParam(name = DESIGNER_VERSION, required = false) String designerVersion) { int result = productMarketplaceDataService.updateInstallationCountForProduct(productId, designerVersion); return new ResponseEntity<>(result, HttpStatus.OK); } + } diff --git a/marketplace-service/src/main/java/com/axonivy/market/logging/Loggable.java b/marketplace-service/src/main/java/com/axonivy/market/logging/Loggable.java new file mode 100644 index 000000000..3194ee816 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/logging/Loggable.java @@ -0,0 +1,11 @@ +package com.axonivy.market.logging; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Loggable { +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/logging/LoggableAspect.java b/marketplace-service/src/main/java/com/axonivy/market/logging/LoggableAspect.java new file mode 100644 index 000000000..059a19983 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/logging/LoggableAspect.java @@ -0,0 +1,92 @@ +package com.axonivy.market.logging; + +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.LoggingConstants; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.log4j.Log4j2; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import static com.axonivy.market.util.FileUtils.createFile; +import static com.axonivy.market.util.FileUtils.writeToFile; +import static com.axonivy.market.util.LoggingUtils.*; + +@Log4j2 +@Aspect +@Component +public class LoggableAspect { + + @Value("${loggable.log-path}") + public String logFilePath; + + @Before("@annotation(com.axonivy.market.logging.Loggable)") + public void logMethodCall(JoinPoint joinPoint) throws MissingHeaderException { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + ServletRequestAttributes attributes = + (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + HttpServletRequest request = attributes.getRequest(); + Map headersMap = extractHeaders(request, signature, joinPoint); + saveLogToDailyFile(headersMap); + + // block execution if request isn't from Market or Ivy Designer + if (!LoggingConstants.MARKET_WEBSITE.equals(headersMap.get(CommonConstants.REQUESTED_BY))) { + throw new MissingHeaderException(); + } + } + } + + private Map extractHeaders(HttpServletRequest request, MethodSignature signature, + JoinPoint joinPoint) { + return Map.of( + LoggingConstants.METHOD, escapeXml(String.valueOf(signature.getMethod())), + LoggingConstants.TIMESTAMP, escapeXml(getCurrentTimestamp()), + CommonConstants.USER_AGENT, escapeXml(request.getHeader(CommonConstants.USER_AGENT)), + LoggingConstants.ARGUMENTS, escapeXml(getArgumentsString(signature.getParameterNames(), joinPoint.getArgs())), + CommonConstants.REQUESTED_BY, escapeXml(request.getHeader(CommonConstants.REQUESTED_BY)) + ); + } + + // Use synchronized to prevent race condition + private synchronized void saveLogToDailyFile(Map headersMap) { + try { + File logFile = createFile(generateFileName()); + + StringBuilder content = new StringBuilder(); + if (logFile.exists()) { + content.append(new String(Files.readAllBytes(logFile.toPath()))); + } + if (content.isEmpty()) { + content.append(LoggingConstants.LOG_START); + } + int lastLogIndex = content.lastIndexOf(LoggingConstants.LOG_END); + if (lastLogIndex != -1) { + content.delete(lastLogIndex, content.length()); + } + content.append(buildLogEntry(headersMap)); + content.append(LoggingConstants.LOG_END); + + writeToFile(logFile, content.toString()); + } catch (IOException e) { + log.error("Error writing log to file: {}", e.getMessage()); + } + } + + private String generateFileName() { + return Path.of(logFilePath, "log-" + getCurrentDate() + ".xml").toString(); + } + +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/FileUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/FileUtils.java new file mode 100644 index 000000000..6eeb35248 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/util/FileUtils.java @@ -0,0 +1,31 @@ +package com.axonivy.market.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class FileUtils { + + public static File createFile(String fileName) throws IOException { + File file = new File(fileName); + File parentDir = file.getParentFile(); + if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) { + throw new IOException("Failed to create directory: " + parentDir.getAbsolutePath()); + } + if (!file.exists() && !file.createNewFile()) { + throw new IOException("Failed to create file: " + file.getAbsolutePath()); + } + return file; + } + + public static void writeToFile(File file, String content) throws IOException { + try (FileWriter writer = new FileWriter(file, false)) { + writer.write(content); + } + } + +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/LoggingUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/LoggingUtils.java new file mode 100644 index 000000000..e69929bc1 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/util/LoggingUtils.java @@ -0,0 +1,54 @@ +package com.axonivy.market.util; + +import com.axonivy.market.constants.LoggingConstants; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +import java.text.SimpleDateFormat; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class LoggingUtils { + + public static String getCurrentDate() { + return new SimpleDateFormat(LoggingConstants.DATE_FORMAT).format(System.currentTimeMillis()); + } + + public static String getCurrentTimestamp() { + return new SimpleDateFormat(LoggingConstants.TIMESTAMP_FORMAT).format(System.currentTimeMillis()); + } + + public static String escapeXml(String value) { + if (StringUtils.isEmpty(value)) { + return StringUtils.EMPTY; + } + return value.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + public static String getArgumentsString(String[] paramNames, Object[] args) { + if (paramNames == null || paramNames.length == 0 || args == null || args.length == 0) { + return LoggingConstants.NO_ARGUMENTS; + } + return IntStream.range(0, paramNames.length) + .mapToObj(i -> paramNames[i] + ": " + args[i]) + .collect(Collectors.joining(", ")); + } + + public static String buildLogEntry(Map headersMap) { + StringBuilder logEntry = new StringBuilder(); + Map map = new TreeMap<>(headersMap); + logEntry.append(LoggingConstants.ENTRY_START); + map.forEach((key, value) -> logEntry.append(String.format(LoggingConstants.ENTRY_FORMAT, key, value, key))); + logEntry.append(LoggingConstants.ENTRY_END); + return logEntry.toString(); + } + +} diff --git a/marketplace-service/src/main/resources/application.properties b/marketplace-service/src/main/resources/application.properties index 3e81b354f..a0ab0a261 100644 --- a/marketplace-service/src/main/resources/application.properties +++ b/marketplace-service/src/main/resources/application.properties @@ -16,4 +16,5 @@ market.github.oauth2-clientSecret=${MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET} jwt.secret=${MARKET_JWT_SECRET_KEY} jwt.expiration=365 logging.level.org.springframework.data.mongodb.core.MongoTemplate=${MARKET_MONGO_LOG_LEVEL} -spring.jackson.serialization.indent_output=true \ No newline at end of file +spring.jackson.serialization.indent_output=true +loggable.log-path=${MARKET_LOG_PATH} \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/logging/LoggableAspectTest.java b/marketplace-service/src/test/java/com/axonivy/market/logging/LoggableAspectTest.java new file mode 100644 index 000000000..0270dfd60 --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/logging/LoggableAspectTest.java @@ -0,0 +1,89 @@ +package com.axonivy.market.logging; + +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.LoggingConstants; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.util.LoggingUtils; +import jakarta.servlet.http.HttpServletRequest; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class LoggableAspectTest { + + @Mock + private HttpServletRequest request; + + private LoggableAspect loggableAspect; + + @BeforeEach + void setUp() throws IOException { + MockitoAnnotations.openMocks(this); + loggableAspect = new LoggableAspect(); + loggableAspect.logFilePath = Files.createTempDirectory("logs").toString(); + } + + @AfterEach + void tearDown() { + RequestContextHolder.resetRequestAttributes(); + } + + @Test + void testLogFileCreation() throws Exception { + mockRequestAttributes(LoggingConstants.MARKET_WEBSITE, "test-agent"); + MethodSignature signature = mockMethodSignature(); + + loggableAspect.logMethodCall(mockJoinPoint(signature)); + Path logFilePath = Path.of(loggableAspect.logFilePath, "log-" + LoggingUtils.getCurrentDate() + ".xml"); + assertTrue(Files.exists(logFilePath), "Log file should be created"); + + String content = Files.readString(logFilePath); + assertTrue(content.contains(LoggingConstants.LOG_START), "Log file should contain log"); + assertTrue(content.contains(LoggingConstants.ENTRY_START), "Log file should contain log entry"); + } + + @Test + void testMissingHeaderException() { + mockRequestAttributes("invalid-source", "mock-agent"); + MethodSignature signature = mockMethodSignature(); + + assertThrows(MissingHeaderException.class, () -> + loggableAspect.logMethodCall(mockJoinPoint(signature)) + ); + } + + private JoinPoint mockJoinPoint(MethodSignature signature) { + JoinPoint joinPoint = mock(JoinPoint.class); + when(joinPoint.getSignature()).thenReturn(signature); + when(joinPoint.getArgs()).thenReturn(new Object[]{"arg1", "arg2"}); + return joinPoint; + } + + private void mockRequestAttributes(String requestedBy, String userAgent) { + when(request.getHeader(CommonConstants.REQUESTED_BY)).thenReturn(requestedBy); + when(request.getHeader(CommonConstants.USER_AGENT)).thenReturn(userAgent); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + } + + private MethodSignature mockMethodSignature() { + MethodSignature signature = mock(MethodSignature.class); + when(signature.getMethod()).thenReturn(this.getClass().getMethods()[0]); + return signature; + } + +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/SchedulingTasksTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/SchedulingTasksTest.java index 23daae4b9..be16eada3 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/SchedulingTasksTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/SchedulingTasksTest.java @@ -13,7 +13,8 @@ @SpringBootTest(properties = {"MONGODB_USERNAME=user", "MONGODB_PASSWORD=password", "MONGODB_HOST=mongoHost", "MONGODB_DATABASE=product", "MARKET_GITHUB_OAUTH_APP_CLIENT_ID=clientId", "MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=clientSecret", "MARKET_JWT_SECRET_KEY=jwtSecret", - "MARKET_CORS_ALLOWED_ORIGIN=*", "MARKET_GITHUB_MARKET_BRANCH=master", "MARKET_MONGO_LOG_LEVEL=DEBUG"}) + "MARKET_CORS_ALLOWED_ORIGIN=*", "MARKET_GITHUB_MARKET_BRANCH=master", "MARKET_MONGO_LOG_LEVEL=DEBUG", + "MARKET_LOG_PATH=logs"}) class SchedulingTasksTest { @SpyBean diff --git a/marketplace-service/src/test/java/com/axonivy/market/util/FileUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/FileUtilsTest.java new file mode 100644 index 000000000..196152d82 --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/util/FileUtilsTest.java @@ -0,0 +1,57 @@ +package com.axonivy.market.util; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class FileUtilsTest { + + private static final String FILE_PATH = "src/test/resources/test-file.xml"; + + @Test + void testCreateFile() throws IOException { + File createdFile = FileUtils.createFile(FILE_PATH); + assertTrue(createdFile.exists(), "File should exist"); + assertTrue(createdFile.isFile(), "Should be a file"); + createdFile.delete(); + } + + @Test + void testFailedToCreateDirectory() { + File createdFile = new File("testDirAsFile"); + try { + if (!createdFile.exists()) { + assertTrue(createdFile.createNewFile(), "Setup failed: could not create file"); + } + + IOException exception = assertThrows(IOException.class, () -> + FileUtils.createFile("testDirAsFile/subDir/testFile.txt") + ); + assertTrue(exception.getMessage().contains("Failed to create directory"), + "Exception message does not contain expected text"); + } catch (IOException e) { + fail("Setup failed: " + e.getMessage()); + } finally { + createdFile.delete(); + } + } + + @Test + void testWriteFile() throws IOException { + File createdFile = FileUtils.createFile(FILE_PATH); + String content = "Hello, world!"; + FileUtils.writeToFile(createdFile, content); + String fileContent = Files.readString(createdFile.toPath()); + assertEquals(content, fileContent, "File content should match the written content"); + createdFile.delete(); + + } + +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/util/LoggingUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/LoggingUtilsTest.java new file mode 100644 index 000000000..be0ad64fa --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/util/LoggingUtilsTest.java @@ -0,0 +1,78 @@ +package com.axonivy.market.util; + +import com.axonivy.market.constants.LoggingConstants; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; + +@ExtendWith(MockitoExtension.class) +class LoggingUtilsTest { + + @Test + void testEscapeXmlSuccess() { + String input = ""; + String expectedValue = "<Test'& "Method>"; + String result = LoggingUtils.escapeXml(input); + Assertions.assertEquals(expectedValue, result); + } + + @Test + void testEscapeXmlOnNullValue() { + String expectedValue = ""; + String result = LoggingUtils.escapeXml(null); + Assertions.assertEquals(expectedValue, result); + } + + @Test + void testGetArgumentsString() { + String expectedValue = "a: random, b: sample"; + String result = LoggingUtils.getArgumentsString(new String[]{"a", "b"}, new String[]{"random", "sample"}); + Assertions.assertEquals(expectedValue, result); + } + + @Test + void testGetArgumentsStringOnNullValue() { + String result = LoggingUtils.getArgumentsString(null, null); + Assertions.assertEquals(LoggingConstants.NO_ARGUMENTS, result); + } + + @Test + void testBuildLogEntry() { + Map given = Map.of( + "method", "test", + "timestamp", "15:02:00" + ); + String expected = """ + + test + 15:02:00 + + """.indent(2); + + var result = LoggingUtils.buildLogEntry(given); + Assertions.assertEquals(expected, result); + } + + @Test + void testGetCurrentDate() { + String expectedDate = LocalDate.now().toString(); + String actualDate = LoggingUtils.getCurrentDate(); + Assertions.assertEquals(expectedDate, actualDate, "The returned date does not match the current date"); + } + + @Test + void testGetCurrentTimestamp() { + String expectedTimestamp = LocalDateTime.now() + .format(DateTimeFormatter.ofPattern(LoggingConstants.TIMESTAMP_FORMAT)); + String actualTimestamp = LoggingUtils.getCurrentTimestamp(); + Assertions.assertEquals(expectedTimestamp.substring(0, 19), actualTimestamp.substring(0, 19), + "The returned timestamp does not match the expected format or value"); + } + +}