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 d10685938..c6b8e3f6b 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 @@ -15,4 +15,6 @@ public class CommonConstants { public static final String BEARER = "Bearer"; public static final String DIGIT_REGEX = "([0-9]+.*)"; public static final String ID_WITH_NUMBER_PATTERN = "%s-%s"; + public static final String ERROR = "error"; + public static final String MESSAGE = "message"; } 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 4ad9f831a..431d7311e 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 @@ -6,4 +6,5 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) 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)"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java index 928f2e203..afc4aba6d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java @@ -4,8 +4,11 @@ import static com.axonivy.market.constants.RequestMappingConstants.GIT_HUB_LOGIN; import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -54,12 +57,16 @@ public ResponseEntity> gitHubLogin(@RequestBody Oauth2Author try { GitHubAccessTokenResponse tokenResponse = gitHubService.getAccessToken(oauth2AuthorizationCode.getCode(), gitHubProperty); accessToken = tokenResponse.getAccessToken(); + User user = gitHubService.getAndUpdateUser(accessToken); + String jwtToken = jwtService.generateToken(user); + return new ResponseEntity<>(Collections.singletonMap(GitHubConstants.Json.TOKEN, jwtToken), HttpStatus.OK); + } catch (Oauth2ExchangeCodeException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(CommonConstants.ERROR, e.getError()); + errorResponse.put(CommonConstants.MESSAGE, e.getErrorDescription()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); } catch (Exception e) { - return new ResponseEntity<>(Map.of(e.getClass().getName(), e.getMessage()), HttpStatus.BAD_REQUEST); + return new ResponseEntity<>(Map.of(CommonConstants.MESSAGE, e.getMessage()), HttpStatus.BAD_REQUEST); } - - User user = gitHubService.getAndUpdateUser(accessToken); - String jwtToken = jwtService.generateToken(user); - return new ResponseEntity<>(Collections.singletonMap(GitHubConstants.Json.TOKEN, jwtToken), HttpStatus.OK); } } \ No newline at end of file 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 2a1610e72..4ff0bc3e8 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,20 +1,26 @@ package com.axonivy.market.github.service.impl; -import static org.apache.commons.lang3.StringUtils.EMPTY; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; - import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.ErrorMessageConstants; +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.User; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; +import com.axonivy.market.exceptions.model.UnauthorizedException; +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.GitHub; import org.kohsuke.github.GitHubBuilder; -import org.kohsuke.github.GHTag; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; @@ -29,19 +35,15 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; -import com.axonivy.market.constants.GitHubConstants; -import com.axonivy.market.entity.User; -import com.axonivy.market.enums.ErrorCode; -import com.axonivy.market.exceptions.model.MissingHeaderException; -import com.axonivy.market.exceptions.model.NotFoundException; -import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; -import com.axonivy.market.exceptions.model.UnauthorizedException; -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 java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.apache.commons.lang3.StringUtils.EMPTY; +@Log4j2 @Service public class GitHubServiceImpl implements GitHubService { @@ -58,9 +60,8 @@ public GitHubServiceImpl(RestTemplateBuilder restTemplateBuilder, UserRepository @Override public GitHub getGitHub() throws IOException { - return new GitHubBuilder() - .withOAuthToken(Optional.ofNullable(gitHubProperty).map(GitHubProperty::getToken).orElse(EMPTY).trim()) - .build(); + return new GitHubBuilder().withOAuthToken( + Optional.ofNullable(gitHubProperty).map(GitHubProperty::getToken).orElse(EMPTY).trim()).build(); } @Override @@ -110,6 +111,8 @@ public GitHubAccessTokenResponse getAccessToken(String code, GitHubProperty gitH GitHubAccessTokenResponse response = responseEntity.getBody(); if (response != null && response.getError() != null && !response.getError().isBlank()) { + log.error(String.format(ErrorMessageConstants.CURRENT_CLIENT_ID_MISMATCH_MESSAGE, code, + gitHubProperty.getOauth2ClientId())); throw new Oauth2ExchangeCodeException(response.getError(), response.getErrorDescription()); } @@ -176,8 +179,8 @@ public List> getUserOrganizations(String accessToken) throws 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())); + ErrorCode.GITHUB_USER_UNAUTHORIZED.getHelpText() + CommonConstants.DASH_SEPARATOR + + GitHubUtils.extractMessageFromExceptionMessage(exception.getMessage())); } } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java b/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java index bcb111ab7..fc6ae5bc4 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java @@ -82,7 +82,7 @@ public static String extractMessageFromExceptionMessage(String exceptionMessage) return StringUtils.EMPTY; } - private static String extractJson(String text) { + public static String extractJson(String text) { int start = text.indexOf("{"); int end = text.lastIndexOf("}") + 1; if (start != -1 && end != -1) { diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java index cc4ae1bf8..5e3c04f55 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java @@ -1,26 +1,28 @@ package com.axonivy.market.controller; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import java.util.Map; - +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; +import com.axonivy.market.github.model.GitHubAccessTokenResponse; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.Oauth2AuthorizationCode; +import com.axonivy.market.service.JwtService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import com.axonivy.market.entity.User; -import com.axonivy.market.exceptions.model.MissingHeaderException; -import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; -import com.axonivy.market.github.model.GitHubAccessTokenResponse; -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.model.Oauth2AuthorizationCode; -import com.axonivy.market.service.JwtService; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class OAuth2ControllerTest { @@ -43,7 +45,7 @@ void setup() { } @Test - void testGitHubLogin() throws Oauth2ExchangeCodeException, MissingHeaderException { + void testGitHubLogin_Success() throws Oauth2ExchangeCodeException, MissingHeaderException { String accessToken = "sampleAccessToken"; User user = createUserMock(); String jwtToken = "sampleJwtToken"; @@ -58,6 +60,42 @@ void testGitHubLogin() throws Oauth2ExchangeCodeException, MissingHeaderExceptio assertEquals(Map.of("token", jwtToken), response.getBody()); } + @Test + void testGitHubLogin_Oauth2ExchangeCodeException() throws Oauth2ExchangeCodeException, MissingHeaderException { + when(gitHubService.getAccessToken(any(), any())).thenThrow( + new Oauth2ExchangeCodeException("invalid_grant", "Invalid authorization code")); + + ResponseEntity response = oAuth2Controller.gitHubLogin(oauth2AuthorizationCode); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + Map body = (Map) response.getBody(); + assertEquals("invalid_grant", body.get(CommonConstants.ERROR)); + assertEquals("Invalid authorization code", body.get(CommonConstants.MESSAGE)); + } + + @Test + void testGitHubLogin_GeneralException() throws Oauth2ExchangeCodeException, MissingHeaderException { + when(gitHubService.getAccessToken(any(), any())).thenThrow(new RuntimeException("Unexpected error")); + + ResponseEntity response = oAuth2Controller.gitHubLogin(oauth2AuthorizationCode); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + Map body = (Map) response.getBody(); + assertTrue(body.containsKey(CommonConstants.MESSAGE)); + assertEquals("Unexpected error", body.get(CommonConstants.MESSAGE)); + } + + @Test + void testGitHubLogin_EmptyAuthorizationCode() { + oauth2AuthorizationCode.setCode(null); + + ResponseEntity response = oAuth2Controller.gitHubLogin(oauth2AuthorizationCode); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + Map body = (Map) response.getBody(); + assertTrue(body.containsKey(CommonConstants.MESSAGE)); + } + private User createUserMock() { User user = new User(); user.setId("userId"); @@ -73,4 +111,4 @@ private GitHubAccessTokenResponse createGitHubAccessTokenResponseMock() { gitHubAccessTokenResponse.setAccessToken("sampleAccessToken"); return gitHubAccessTokenResponse; } -} \ No newline at end of file +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java b/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java index b5b16ed94..d440e0215 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java @@ -5,6 +5,8 @@ import com.axonivy.market.exceptions.model.MissingHeaderException; import com.axonivy.market.exceptions.model.NoContentException; import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; +import com.axonivy.market.exceptions.model.UnauthorizedException; import com.axonivy.market.model.Message; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -61,4 +63,18 @@ void testHandleInvalidException() { var responseEntity = exceptionHandlers.handleInvalidException(invalidParamException); assertEquals(HttpStatus.BAD_REQUEST, responseEntity.getStatusCode()); } + + @Test + void testHandleOauth2ExchangeCodeException() { + var oauth2ExchangeCodeException = mock(Oauth2ExchangeCodeException.class); + var responseEntity = exceptionHandlers.handleOauth2ExchangeCodeException(oauth2ExchangeCodeException); + assertEquals(HttpStatus.BAD_REQUEST, responseEntity.getStatusCode()); + } + + @Test + void testHandleUnauthorizedException() { + var unauthorizedException = mock(UnauthorizedException.class); + var responseEntity = exceptionHandlers.handleUnauthorizedException(unauthorizedException); + assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java index 2f97404dd..d0ffc76ce 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java @@ -90,4 +90,40 @@ void testGetNonStandardImageFolder() { result = GitHubUtils.getNonStandardImageFolder(JIRA_CONNECTOR); Assertions.assertEquals("images", result); } + + @Test + void testExtractJson() { + // Test case: valid JSON inside a string + String exceptionMessage = "Error occurred: {\"message\":\"An error occurred\"}"; + String json = GitHubUtils.extractJson(exceptionMessage); + Assertions.assertEquals("{\"message\":\"An error occurred\"}", json); + + // Test case: no JSON in string + exceptionMessage = "Error occurred: no json here"; + json = GitHubUtils.extractJson(exceptionMessage); + Assertions.assertEquals(StringUtils.EMPTY, json); + + // Test case: empty string + exceptionMessage = ""; + json = GitHubUtils.extractJson(exceptionMessage); + Assertions.assertEquals(StringUtils.EMPTY, json); + } + + @Test + void testExtractMessageFromExceptionMessage() { + // Test case: valid message extraction + String exceptionMessage = "Some error occurred: {\"message\":\"Invalid input data\"}"; + String extractedMessage = GitHubUtils.extractMessageFromExceptionMessage(exceptionMessage); + Assertions.assertEquals("Invalid input data", extractedMessage); + + // Test case: no message key + exceptionMessage = "Some error occurred: {\"error\":\"Something went wrong\"}"; + extractedMessage = GitHubUtils.extractMessageFromExceptionMessage(exceptionMessage); + Assertions.assertEquals(StringUtils.EMPTY, extractedMessage); + + // Test case: empty exception message + exceptionMessage = ""; + extractedMessage = GitHubUtils.extractMessageFromExceptionMessage(exceptionMessage); + Assertions.assertEquals(StringUtils.EMPTY, extractedMessage); + } }