diff --git a/src/main/java/com/eactive/eai/manage/oauthtoken/OAuthTokenStatusDTO.java b/src/main/java/com/eactive/eai/manage/oauthtoken/OAuthTokenStatusDTO.java index 7b7806b..397baa1 100644 --- a/src/main/java/com/eactive/eai/manage/oauthtoken/OAuthTokenStatusDTO.java +++ b/src/main/java/com/eactive/eai/manage/oauthtoken/OAuthTokenStatusDTO.java @@ -1,5 +1,7 @@ package com.eactive.eai.manage.oauthtoken; +import java.util.Map; + import lombok.Data; /** @@ -18,9 +20,22 @@ public class OAuthTokenStatusDTO { /** OAuth 인증 정보 사용 여부 (Y/N) */ String useYn; - /** 토큰 발급에 사용하는 구현체 클래스명 (어댑터 속성 ADAPTER_TOKEN_ISSUING_CLIENT_TYPE) */ + /** + * 스케줄러가 토큰을 발급할 때 사용하는 구현체 클래스명. + * + * AccessTokenManagerByDB.issueToken() 이 그룹의 어댑터 중 getAdapters().next() 하나만 골라 + * 그 어댑터의 ADAPTER_TOKEN_ISSUING_CLIENT_TYPE 을 쓰므로 여기서도 같은 방식으로 조회한다. + */ String tokenServiceClass; + /** + * 그룹에 속한 어댑터별 ADAPTER_TOKEN_ISSUING_CLIENT_TYPE. (어댑터명 → 클래스명) + * + * 어댑터마다 값이 다르면 스케줄러가 쓰는 구현체와 거래 중 재발급(retryAccessTokenVO) 때 + * 쓰이는 구현체가 달라진다. 그 불일치를 확인하기 위한 항목이다. + */ + Map adapterTokenServiceClasses; + /** 토큰 발급 URL */ String tokenUrl; diff --git a/src/main/java/com/eactive/eai/manage/oauthtoken/OAuthTokenStatusService.java b/src/main/java/com/eactive/eai/manage/oauthtoken/OAuthTokenStatusService.java index 131e33f..e6c9bcd 100644 --- a/src/main/java/com/eactive/eai/manage/oauthtoken/OAuthTokenStatusService.java +++ b/src/main/java/com/eactive/eai/manage/oauthtoken/OAuthTokenStatusService.java @@ -3,7 +3,10 @@ package com.eactive.eai.manage.oauthtoken; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Properties; import org.apache.commons.lang3.StringUtils; @@ -75,6 +78,7 @@ public class OAuthTokenStatusService { dto.setTokenUrl(credential.getUrl()); dto.setIntervalSec(credential.getIntervalSec()); dto.setTokenServiceClass(findTokenServiceClass(adapterGroupName)); + dto.setAdapterTokenServiceClasses(findAdapterTokenServiceClasses(adapterGroupName)); AccessTokenVO accessToken; try { @@ -113,6 +117,15 @@ public class OAuthTokenStatusService { dto.setClientId(oauth2Token.getClientId()); } + // 만료시각이 없는 토큰은 isExpired() 가 false 라 재발급 대상이 되지 않는다. + // 빈 토큰이 캐시에 들어가면 기동 후 계속 그대로 사용되므로 조회 시 눈에 띄게 알린다. + if (StringUtils.isBlank(accessToken.getAccessToken())) { + dto.setMessage("캐시에 빈 토큰이 있습니다. 발급에 실패했는데도 구현체가 빈 토큰을 반환한 것으로 보입니다." + + " 만료시각이 없어 자동 재발급되지 않습니다."); + } else if (expiration == null) { + dto.setMessage("만료시각이 없어 자동 재발급되지 않습니다."); + } + return dto; } @@ -139,6 +152,37 @@ public class OAuthTokenStatusService { } } + /** + * 그룹에 속한 어댑터별로 토큰 발급 구현체 클래스명을 모은다. + * + * 스케줄러(issueToken)는 getAdapters().next() 로 어댑터 하나만 골라 쓰고, 거래 중 재발급 + * (retryAccessTokenVO)은 호출한 어댑터 자신의 속성을 쓴다. 어댑터마다 설정이 다르면 + * 서로 다른 구현체가 동작하므로 전체를 보여준다. + * + * @param adapterGroupName 어댑터그룹명 + * @return 어댑터명 → 구현체 클래스명. 조회 실패 시 빈 Map. + */ + private Map findAdapterTokenServiceClasses(String adapterGroupName) { + Map result = new LinkedHashMap(); + try { + AdapterGroupVO gvo = AdapterManager.getInstance().getAdapterGroupVO(adapterGroupName); + if (gvo == null) { + return result; + } + Iterator adapters = gvo.getAdapters(); + while (adapters.hasNext()) { + AdapterVO avo = adapters.next(); + Properties properties = AdapterPropManager.getInstance().getProperties(avo.getPropGroupName()); + result.put(avo.getName(), properties.getProperty(PROP_TOKEN_SERVICE_CLASS)); + } + } catch (Exception e) { + if (logger.isDebug()) { + logger.debug("어댑터별 토큰 발급 구현체 조회 실패. adapterGroupName : " + adapterGroupName); + } + } + return result; + } + /** * accessToken 앞 8자만 남기고 마스킹한다. * diff --git a/src/test/java/com/eactive/eai/manage/oauthtoken/OAuthTokenStatusServiceTest.java b/src/test/java/com/eactive/eai/manage/oauthtoken/OAuthTokenStatusServiceTest.java index 28d3708..aee02be 100644 --- a/src/test/java/com/eactive/eai/manage/oauthtoken/OAuthTokenStatusServiceTest.java +++ b/src/test/java/com/eactive/eai/manage/oauthtoken/OAuthTokenStatusServiceTest.java @@ -3,6 +3,7 @@ package com.eactive.eai.manage.oauthtoken; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyString; @@ -178,6 +179,36 @@ class OAuthTokenStatusServiceTest { assertFalse(dto.toString().contains("super-secret"), "clientSecret 이 응답에 포함되면 안 된다"); } + @Test + @DisplayName("8-1. 캐시에 빈 토큰이 있으면 경고 메시지를 담는다") + void 빈토큰경고() { + registerCredential(GROUP); + // 기존 구현체는 발급에 실패해도 빈 OAuth2AccessTokenVO 를 반환하고, 그게 그대로 캐시된다. + Mockito.when(mockManager.peekAccessTokenVO(GROUP)).thenReturn(new OAuth2AccessTokenVO()); + + OAuthTokenStatusDTO dto = service.getStatus(GROUP); + + assertTrue(dto.isCached()); + assertFalse(dto.isExpired()); + assertEquals(0, dto.getAccessTokenLength()); + assertTrue(dto.getMessage().contains("빈 토큰")); + } + + @Test + @DisplayName("8-2. 만료시각이 없는 토큰은 재발급되지 않음을 알린다") + void 만료시각없음경고() { + registerCredential(GROUP); + OAuth2AccessTokenVO vo = new OAuth2AccessTokenVO(); + vo.setAccessToken("abcdefghijkl"); + Mockito.when(mockManager.peekAccessTokenVO(GROUP)).thenReturn(vo); + + OAuthTokenStatusDTO dto = service.getStatus(GROUP); + + assertTrue(dto.isCached()); + assertNull(dto.getExpiration()); + assertTrue(dto.getMessage().contains("재발급")); + } + @Test @DisplayName("9. 전체 목록은 등록된 어댑터그룹 수만큼 반환한다") void 전체목록() {