From afe2040b6ec4ee02f5ee295975213df0b72bfb1e Mon Sep 17 00:00:00 2001 From: curry772 Date: Tue, 21 Jul 2026 15:25:27 +0900 Subject: [PATCH] =?UTF-8?q?SessionManager=20=EC=BA=90=EC=8B=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B4=80=EB=A6=AC=20API=20=EC=B6=94=EA=B0=80=20(/m?= =?UTF-8?q?anage/session)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SessionManagerForIgnite/SessionManagerForEhcache가 관리하는 login, http-login, evict-master, terminal, convert-user-id, socket, outbound-access-token 캐시를 조회할 수 있는 관리용 API를 추가한다. outbound-access-token 캐시는 Ignite 백엔드에서만 지원하며, Ehcache 백엔드에서는 success=false로 응답하고 500을 반환하지 않는다. --- ...SessionCacheManage.postman_collection.json | 312 ++++++++++++++++++ .../eai/common/session/SessionManager.java | 2 + .../session/SessionManagerForEhcache.java | 7 +- .../session/SessionManagerForIgnite.java | 5 + .../session/SessionCacheManageController.java | 104 ++++++ .../session/SessionCacheManageService.java | 71 ++++ .../SessionCacheManageControllerTest.java | 200 +++++++++++ .../SessionCacheManageServiceTest.java | 185 +++++++++++ 8 files changed, 885 insertions(+), 1 deletion(-) create mode 100644 postman/SessionCacheManage.postman_collection.json create mode 100644 src/main/java/com/eactive/eai/manage/session/SessionCacheManageController.java create mode 100644 src/main/java/com/eactive/eai/manage/session/SessionCacheManageService.java create mode 100644 src/test/java/com/eactive/eai/manage/session/SessionCacheManageControllerTest.java create mode 100644 src/test/java/com/eactive/eai/manage/session/SessionCacheManageServiceTest.java diff --git a/postman/SessionCacheManage.postman_collection.json b/postman/SessionCacheManage.postman_collection.json new file mode 100644 index 0000000..435d42f --- /dev/null +++ b/postman/SessionCacheManage.postman_collection.json @@ -0,0 +1,312 @@ +{ + "info": { + "_postman_id": "c3f4a1e0-9b2d-4a11-8e5a-session-cache-0001", + "name": "SessionCacheManage API (/manage/session)", + "description": "SessionManager(SessionManagerForIgnite/SessionManagerForEhcache) 캐시 조회 API 샘플 모음.\n\n사용 전 확인사항:\n1. Collection Variables 의 baseUrl 을 실제 서버 주소로 변경\n2. 서버가 Ignite 캐시 백엔드로 기동된 경우에만 outbound-access-token 캐시가 채워짐 (Ehcache 백엔드는 success=false로 응답, 정상 동작임)\n3. 모든 요청은 조회 전용(GET)이며 실제 세션/캐시 데이터를 변경하지 않는다.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { "key": "baseUrl", "value": "http://localhost:8080", "type": "string" } + ], + "item": [ + { + "name": "1. 캐시 요약 / 마스터 인스턴스", + "item": [ + { + "name": "전체 캐시 요약 (구현체별 XML)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/manage/session/cache/status", + "host": ["{{baseUrl}}"], + "path": ["manage", "session", "cache", "status"] + }, + "description": "Ignite/Ehcache 구현체가 각자 형식으로 만든 전체 캐시 이름/크기 요약 텍스트(XML)를 반환한다." + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "pm.test('status 200', () => pm.response.to.have.status(200));", + "pm.test('success = true', () => pm.expect(json.success).to.eql(true));", + "pm.test('data는 문자열(XML)', () => pm.expect(json.data).to.be.a('string'));", + "pm.test('Caches 루트 태그 포함', () => pm.expect(json.data).to.include(' pm.response.to.have.status(200));", + "pm.test('success = true', () => pm.expect(json.success).to.eql(true));", + "console.log('master instance id:', json.data);" + ] + } + } + ] + } + ] + }, + { + "name": "2. 개별 캐시 조회", + "item": [ + { + "name": "로그인(WebSocket) 캐시", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/manage/session/cache/login", + "host": ["{{baseUrl}}"], + "path": ["manage", "session", "cache", "login"] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "pm.test('status 200', () => pm.response.to.have.status(200));", + "pm.test('success = true', () => pm.expect(json.success).to.eql(true));", + "pm.test('name = loginSession', () => pm.expect(json.data.name).to.eql('loginSession'));", + "pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));", + "pm.test('size는 keys 개수와 일치', () => pm.expect(json.data.size).to.eql(json.data.keys.length));" + ] + } + } + ] + }, + { + "name": "HTTP 로그인 캐시", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/manage/session/cache/http-login", + "host": ["{{baseUrl}}"], + "path": ["manage", "session", "cache", "http-login"] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "pm.test('status 200', () => pm.response.to.have.status(200));", + "pm.test('success = true', () => pm.expect(json.success).to.eql(true));", + "pm.test('name = httpLogin', () => pm.expect(json.data.name).to.eql('httpLogin'));", + "pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));" + ] + } + } + ] + }, + { + "name": "싱글 어댑터 evict-master 캐시", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/manage/session/cache/evict-master", + "host": ["{{baseUrl}}"], + "path": ["manage", "session", "cache", "evict-master"] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "pm.test('status 200', () => pm.response.to.have.status(200));", + "pm.test('success = true', () => pm.expect(json.success).to.eql(true));", + "pm.test('name = evictMasterCache', () => pm.expect(json.data.name).to.eql('evictMasterCache'));", + "pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));" + ] + } + } + ] + }, + { + "name": "단말(ATM 등) 캐시", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/manage/session/cache/terminal", + "host": ["{{baseUrl}}"], + "path": ["manage", "session", "cache", "terminal"] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "pm.test('status 200', () => pm.response.to.have.status(200));", + "pm.test('success = true', () => pm.expect(json.success).to.eql(true));", + "pm.test('name = terminalSession', () => pm.expect(json.data.name).to.eql('terminalSession'));", + "pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));" + ] + } + } + ] + }, + { + "name": "단말ID→사용자ID 변환 캐시", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/manage/session/cache/convert-user-id", + "host": ["{{baseUrl}}"], + "path": ["manage", "session", "cache", "convert-user-id"] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "pm.test('status 200', () => pm.response.to.have.status(200));", + "pm.test('success = true', () => pm.expect(json.success).to.eql(true));", + "pm.test('name = convertUserIdSession', () => pm.expect(json.data.name).to.eql('convertUserIdSession'));", + "pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));" + ] + } + } + ] + }, + { + "name": "소켓 세션 캐시", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/manage/session/cache/socket", + "host": ["{{baseUrl}}"], + "path": ["manage", "session", "cache", "socket"] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "pm.test('status 200', () => pm.response.to.have.status(200));", + "pm.test('success = true', () => pm.expect(json.success).to.eql(true));", + "pm.test('name = adapterSession', () => pm.expect(json.data.name).to.eql('adapterSession'));", + "pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));" + ] + } + } + ] + }, + { + "name": "아웃바운드 AccessToken 캐시 (Ignite 전용)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/manage/session/cache/outbound-access-token", + "host": ["{{baseUrl}}"], + "path": ["manage", "session", "cache", "outbound-access-token"] + }, + "description": "Ignite 백엔드에서만 지원한다. Ehcache 백엔드로 기동된 서버라면 success=false + message로 응답하며, 이는 정상 동작이다(서버 500 아님)." + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "pm.test('status 200', () => pm.response.to.have.status(200));", + "if (json.success) {", + " pm.test('data.name 존재', () => pm.expect(json.data.name).to.be.a('string'));", + " pm.test('data.keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));", + "} else {", + " pm.test('미지원 백엔드는 message 포함', () => pm.expect(json.message).to.be.a('string'));", + " console.log('OutboundAccessToken 캐시 미지원 백엔드:', json.message);", + "}" + ] + } + } + ] + } + ] + }, + { + "name": "3. 전체 캐시 목록", + "item": [ + { + "name": "모든 캐시 한번에 조회", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/manage/session/cache/all", + "host": ["{{baseUrl}}"], + "path": ["manage", "session", "cache", "all"] + }, + "description": "login/http-login/evict-master/terminal/convert-user-id/socket 6종은 항상 포함되고, outbound-access-token은 Ignite 백엔드일 때만 추가되어 count가 6 또는 7이 된다." + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "pm.test('status 200', () => pm.response.to.have.status(200));", + "pm.test('success = true', () => pm.expect(json.success).to.eql(true));", + "pm.test('data는 배열', () => pm.expect(json.data).to.be.an('array'));", + "pm.test('count는 6 또는 7 (백엔드에 따라 outbound-access-token 포함 여부 다름)', () => {", + " pm.expect(json.count).to.be.oneOf([6, 7]);", + "});", + "pm.test('count와 data.length 일치', () => pm.expect(json.count).to.eql(json.data.length));", + "json.data.forEach((cache) => {", + " pm.test(`캐시 [${cache.name}] 는 keys 배열을 가짐`, () => pm.expect(cache.keys).to.be.an('array'));", + "});" + ] + } + } + ] + } + ] + } + ] +} diff --git a/src/main/java/com/eactive/eai/common/session/SessionManager.java b/src/main/java/com/eactive/eai/common/session/SessionManager.java index f3e948d..d237e1e 100644 --- a/src/main/java/com/eactive/eai/common/session/SessionManager.java +++ b/src/main/java/com/eactive/eai/common/session/SessionManager.java @@ -246,6 +246,8 @@ public abstract class SessionManager implements Lifecycle { public abstract CacheInfoVO getSocketCache(); + public abstract CacheInfoVO getOutboundAccessTokenCache(); + public abstract String getMasterInstId(); // Convert terminalId to userId diff --git a/src/main/java/com/eactive/eai/common/session/SessionManagerForEhcache.java b/src/main/java/com/eactive/eai/common/session/SessionManagerForEhcache.java index b488677..e3ae82e 100644 --- a/src/main/java/com/eactive/eai/common/session/SessionManagerForEhcache.java +++ b/src/main/java/com/eactive/eai/common/session/SessionManagerForEhcache.java @@ -887,7 +887,12 @@ public class SessionManagerForEhcache extends SessionManager { public CacheInfoVO getSocketCache() { return convert(cacheSocket); } - + + @Override + public CacheInfoVO getOutboundAccessTokenCache() { + throw new UnsupportedOperationException(); + } + @Override public void putWebSocketTimeout(String key, SessionVO value) { cacheWebSocketTimeout.put(new Element(key, value)); diff --git a/src/main/java/com/eactive/eai/common/session/SessionManagerForIgnite.java b/src/main/java/com/eactive/eai/common/session/SessionManagerForIgnite.java index 064bff9..16b5fa6 100644 --- a/src/main/java/com/eactive/eai/common/session/SessionManagerForIgnite.java +++ b/src/main/java/com/eactive/eai/common/session/SessionManagerForIgnite.java @@ -818,6 +818,11 @@ public class SessionManagerForIgnite extends SessionManager { public CacheInfoVO getSocketCache() { return convert(cacheSocket); } + + @Override + public CacheInfoVO getOutboundAccessTokenCache() { + return convert(cacheOutBoundAccessToken); + } @Override public void putWebSocketTimeout(String key, SessionVO value) { diff --git a/src/main/java/com/eactive/eai/manage/session/SessionCacheManageController.java b/src/main/java/com/eactive/eai/manage/session/SessionCacheManageController.java new file mode 100644 index 0000000..5a02494 --- /dev/null +++ b/src/main/java/com/eactive/eai/manage/session/SessionCacheManageController.java @@ -0,0 +1,104 @@ +package com.eactive.eai.manage.session; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.eactive.eai.common.session.CacheInfoVO; + +/** + * SessionManager(SessionManagerForIgnite/SessionManagerForEhcache) 캐시 상태 모니터링 API. + * + * GET /manage/session/cache/status → 전체 캐시 요약 (구현체별 XML 텍스트, Ignite/Ehcache) + * GET /manage/session/cache/all → 개별 캐시(login/http-login/evict-master/terminal/convert-user-id/socket) 목록 + * GET /manage/session/cache/login → 로그인(WebSocket) 캐시 + * GET /manage/session/cache/http-login → HTTP 로그인 캐시 + * GET /manage/session/cache/evict-master → 싱글 어댑터 evict master 캐시 + * GET /manage/session/cache/terminal → 단말(ATM 등) 캐시 + * GET /manage/session/cache/convert-user-id → 단말ID → 사용자ID 변환 캐시 + * GET /manage/session/cache/socket → 소켓 세션 캐시 + * GET /manage/session/cache/outbound-access-token → 아웃바운드 AccessTokenVO 캐시 (Ignite 백엔드에서만 지원) + * GET /manage/session/master-inst-id → 현재 evict master 인스턴스명 + */ +@RestController +@RequestMapping("/manage/session") +public class SessionCacheManageController { + + @Autowired + private SessionCacheManageService sessionCacheManageService; + + @GetMapping("/cache/status") + public ResponseEntity getCacheStatus() { + return ok(sessionCacheManageService.getCacheStatus()); + } + + @GetMapping("/master-inst-id") + public ResponseEntity getMasterInstId() { + return ok(sessionCacheManageService.getMasterInstId()); + } + + @GetMapping("/cache/all") + public ResponseEntity getAllCaches() { + List list = sessionCacheManageService.getAllCaches(); + Map result = new HashMap<>(); + result.put("success", true); + result.put("data", list); + result.put("count", list.size()); + return ResponseEntity.ok(result); + } + + @GetMapping("/cache/login") + public ResponseEntity getLoginCache() { + return ok(sessionCacheManageService.getLoginCache()); + } + + @GetMapping("/cache/http-login") + public ResponseEntity getHttpLoginCache() { + return ok(sessionCacheManageService.getHttpLoginCache()); + } + + @GetMapping("/cache/evict-master") + public ResponseEntity getEvictMasterCache() { + return ok(sessionCacheManageService.getEvictMasterCache()); + } + + @GetMapping("/cache/terminal") + public ResponseEntity getTerminalCache() { + return ok(sessionCacheManageService.getTerminalCache()); + } + + @GetMapping("/cache/convert-user-id") + public ResponseEntity getConvertUserIdCache() { + return ok(sessionCacheManageService.getConvertUserIdCache()); + } + + @GetMapping("/cache/socket") + public ResponseEntity getSocketCache() { + return ok(sessionCacheManageService.getSocketCache()); + } + + @GetMapping("/cache/outbound-access-token") + public ResponseEntity getOutboundAccessTokenCache() { + try { + return ok(sessionCacheManageService.getOutboundAccessTokenCache()); + } catch (UnsupportedOperationException e) { + Map result = new HashMap<>(); + result.put("success", false); + result.put("message", "현재 SessionManager 백엔드는 OutboundAccessToken 캐시를 지원하지 않습니다."); + return ResponseEntity.ok(result); + } + } + + private ResponseEntity ok(Object data) { + Map result = new HashMap<>(); + result.put("success", true); + result.put("data", data); + return ResponseEntity.ok(result); + } +} diff --git a/src/main/java/com/eactive/eai/manage/session/SessionCacheManageService.java b/src/main/java/com/eactive/eai/manage/session/SessionCacheManageService.java new file mode 100644 index 0000000..0045e7a --- /dev/null +++ b/src/main/java/com/eactive/eai/manage/session/SessionCacheManageService.java @@ -0,0 +1,71 @@ +package com.eactive.eai.manage.session; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.eactive.eai.common.session.CacheInfoVO; +import com.eactive.eai.common.session.SessionManager; + +/** + * SessionManager(SessionManagerForIgnite/SessionManagerForEhcache) 캐시 조회를 위임하는 서비스. + * + * SessionManager.getInstance() 는 설정(session_manager.cacheType)에 따라 Ignite/Ehcache + * 구현체 중 하나의 싱글턴을 반환하며, 두 구현체 모두 아래 조회 메서드를 지원한다. + */ +@Service +public class SessionCacheManageService { + + public String getCacheStatus() { + return SessionManager.getInstance().getCacheStatus(); + } + + public String getMasterInstId() { + return SessionManager.getInstance().getMasterInstId(); + } + + public CacheInfoVO getLoginCache() { + return SessionManager.getInstance().getLoginCache(); + } + + public CacheInfoVO getHttpLoginCache() { + return SessionManager.getInstance().getHttpLoginCache(); + } + + public CacheInfoVO getEvictMasterCache() { + return SessionManager.getInstance().getEvictMasterCache(); + } + + public CacheInfoVO getTerminalCache() { + return SessionManager.getInstance().getTerminalCache(); + } + + public CacheInfoVO getConvertUserIdCache() { + return SessionManager.getInstance().getConvertUserIdCache(); + } + + public CacheInfoVO getSocketCache() { + return SessionManager.getInstance().getSocketCache(); + } + + public CacheInfoVO getOutboundAccessTokenCache() { + return SessionManager.getInstance().getOutboundAccessTokenCache(); + } + + public List getAllCaches() { + List result = new ArrayList<>(); + result.add(getLoginCache()); + result.add(getHttpLoginCache()); + result.add(getEvictMasterCache()); + result.add(getTerminalCache()); + result.add(getConvertUserIdCache()); + result.add(getSocketCache()); + try { + result.add(getOutboundAccessTokenCache()); + } catch (UnsupportedOperationException e) { + // Ehcache 백엔드는 OutboundAccessToken 캐시를 지원하지 않는다. + } + return result; + } +} diff --git a/src/test/java/com/eactive/eai/manage/session/SessionCacheManageControllerTest.java b/src/test/java/com/eactive/eai/manage/session/SessionCacheManageControllerTest.java new file mode 100644 index 0000000..ccde8b6 --- /dev/null +++ b/src/test/java/com/eactive/eai/manage/session/SessionCacheManageControllerTest.java @@ -0,0 +1,200 @@ +package com.eactive.eai.manage.session; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; + +import com.eactive.eai.common.session.CacheInfoVO; + +/** + * SessionCacheManageController 단위 테스트. + * + *

Spring MVC를 기동하지 않고 컨트롤러를 직접 인스턴스화하여 + * 응답 구조(success, data, count, message)를 검증한다. + */ +class SessionCacheManageControllerTest { + + private SessionCacheManageController controller; + private SessionCacheManageService mockService; + + @BeforeEach + void setUp() { + controller = new SessionCacheManageController(); + mockService = mock(SessionCacheManageService.class); + ReflectionTestUtils.setField(controller, "sessionCacheManageService", mockService); + } + + // ------------------------------------------------------------------------- + // Helper + // ------------------------------------------------------------------------- + + private CacheInfoVO makeCacheInfo(String name) { + CacheInfoVO vo = new CacheInfoVO(); + vo.setName(name); + vo.setSize(3); + vo.setKeys(Arrays.asList("a", "b", "c")); + vo.setTimeToLive(60000); + return vo; + } + + @SuppressWarnings("unchecked") + private Map body(ResponseEntity response) { + return (Map) response.getBody(); + } + + // ========================================================================= + // 캐시 요약 / 마스터 인스턴스 + // ========================================================================= + + @Test + @DisplayName("getCacheStatus() — success=true, data에 원문 그대로 반환") + void getCacheStatus_returnsRawStatusString() { + when(mockService.getCacheStatus()).thenReturn(""); + + Map body = body(controller.getCacheStatus()); + + assertEquals(Boolean.TRUE, body.get("success")); + assertEquals("", body.get("data")); + } + + @Test + @DisplayName("getMasterInstId() — success=true, data에 인스턴스명 반환") + void getMasterInstId_returnsInstanceName() { + when(mockService.getMasterInstId()).thenReturn("SERVER01"); + + Map body = body(controller.getMasterInstId()); + + assertEquals(Boolean.TRUE, body.get("success")); + assertEquals("SERVER01", body.get("data")); + } + + // ========================================================================= + // 개별 캐시 조회 + // ========================================================================= + + @Test + @DisplayName("getLoginCache() — success=true, CacheInfoVO 반환") + void getLoginCache_returnsCacheInfo() { + CacheInfoVO vo = makeCacheInfo("LoginCache"); + when(mockService.getLoginCache()).thenReturn(vo); + + ResponseEntity response = controller.getLoginCache(); + Map body = body(response); + + assertEquals(200, response.getStatusCodeValue()); + assertEquals(Boolean.TRUE, body.get("success")); + assertSame(vo, body.get("data")); + } + + @Test + @DisplayName("getHttpLoginCache() — success=true") + void getHttpLoginCache_returnsCacheInfo() { + CacheInfoVO vo = makeCacheInfo("HttpLoginCache"); + when(mockService.getHttpLoginCache()).thenReturn(vo); + + assertSame(vo, body(controller.getHttpLoginCache()).get("data")); + } + + @Test + @DisplayName("getEvictMasterCache() — success=true") + void getEvictMasterCache_returnsCacheInfo() { + CacheInfoVO vo = makeCacheInfo("EvictMasterCache"); + when(mockService.getEvictMasterCache()).thenReturn(vo); + + assertSame(vo, body(controller.getEvictMasterCache()).get("data")); + } + + @Test + @DisplayName("getTerminalCache() — success=true") + void getTerminalCache_returnsCacheInfo() { + CacheInfoVO vo = makeCacheInfo("TerminalCache"); + when(mockService.getTerminalCache()).thenReturn(vo); + + assertSame(vo, body(controller.getTerminalCache()).get("data")); + } + + @Test + @DisplayName("getConvertUserIdCache() — success=true") + void getConvertUserIdCache_returnsCacheInfo() { + CacheInfoVO vo = makeCacheInfo("ConvertUserIdCache"); + when(mockService.getConvertUserIdCache()).thenReturn(vo); + + assertSame(vo, body(controller.getConvertUserIdCache()).get("data")); + } + + @Test + @DisplayName("getSocketCache() — success=true") + void getSocketCache_returnsCacheInfo() { + CacheInfoVO vo = makeCacheInfo("SocketCache"); + when(mockService.getSocketCache()).thenReturn(vo); + + assertSame(vo, body(controller.getSocketCache()).get("data")); + } + + // ========================================================================= + // 전체 캐시 목록 + // ========================================================================= + + @Test + @DisplayName("getAllCaches() — 목록과 개수 반환") + void getAllCaches_returnsListAndCount() { + List list = Arrays.asList(makeCacheInfo("A"), makeCacheInfo("B")); + when(mockService.getAllCaches()).thenReturn(list); + + Map body = body(controller.getAllCaches()); + + assertEquals(Boolean.TRUE, body.get("success")); + assertSame(list, body.get("data")); + assertEquals(2, body.get("count")); + } + + @Test + @DisplayName("getAllCaches() — 빈 목록이면 count=0") + void getAllCaches_emptyList_countZero() { + when(mockService.getAllCaches()).thenReturn(Arrays.asList()); + + Map body = body(controller.getAllCaches()); + + assertEquals(Boolean.TRUE, body.get("success")); + assertEquals(0, body.get("count")); + } + + // ========================================================================= + // OutboundAccessToken 캐시 — 지원/미지원 백엔드 + // ========================================================================= + + @Test + @DisplayName("getOutboundAccessTokenCache() — 지원 시 success=true, CacheInfoVO 반환") + void getOutboundAccessTokenCache_supported_returnsCacheInfo() { + CacheInfoVO vo = makeCacheInfo("OutBoundAccessToken"); + when(mockService.getOutboundAccessTokenCache()).thenReturn(vo); + + Map body = body(controller.getOutboundAccessTokenCache()); + + assertEquals(Boolean.TRUE, body.get("success")); + assertSame(vo, body.get("data")); + } + + @Test + @DisplayName("getOutboundAccessTokenCache() — Ehcache 등 미지원 백엔드는 500이 아니라 success=false로 응답") + void getOutboundAccessTokenCache_unsupported_successFalseNotServerError() { + when(mockService.getOutboundAccessTokenCache()).thenThrow(new UnsupportedOperationException()); + + ResponseEntity response = controller.getOutboundAccessTokenCache(); + Map body = body(response); + + assertEquals(200, response.getStatusCodeValue()); + assertEquals(Boolean.FALSE, body.get("success")); + assertNotNull(body.get("message")); + assertFalse(body.containsKey("data")); + } +} diff --git a/src/test/java/com/eactive/eai/manage/session/SessionCacheManageServiceTest.java b/src/test/java/com/eactive/eai/manage/session/SessionCacheManageServiceTest.java new file mode 100644 index 0000000..b1af8b3 --- /dev/null +++ b/src/test/java/com/eactive/eai/manage/session/SessionCacheManageServiceTest.java @@ -0,0 +1,185 @@ +package com.eactive.eai.manage.session; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.eactive.eai.common.session.CacheInfoVO; +import com.eactive.eai.common.session.SessionManager; + +/** + * SessionCacheManageService 단위 테스트. + * + *

SessionManager.getInstance()는 static singleton(private static instance 필드)이라 + * 실제 Ignite/Ehcache를 기동하지 않고, 리플렉션으로 그 필드를 Mockito mock으로 교체하여 검증한다. + * 테스트가 끝나면 반드시 필드를 원복(null)하여 다른 테스트에 영향을 주지 않는다. + */ +class SessionCacheManageServiceTest { + + private SessionManager mockSessionManager; + private final SessionCacheManageService service = new SessionCacheManageService(); + + @BeforeEach + void setUp() throws Exception { + mockSessionManager = mock(SessionManager.class); + setInstance(mockSessionManager); + } + + @AfterEach + void tearDown() throws Exception { + setInstance(null); + } + + private void setInstance(SessionManager manager) throws Exception { + Field field = SessionManager.class.getDeclaredField("instance"); + field.setAccessible(true); + field.set(null, manager); + } + + private CacheInfoVO makeCacheInfo(String name, int size) { + CacheInfoVO vo = new CacheInfoVO(); + vo.setName(name); + vo.setSize(size); + vo.setKeys(Arrays.asList("k1", "k2")); + vo.setTimeToLive(60000); + return vo; + } + + // ========================================================================= + // 1. 단건 위임 + // ========================================================================= + + @Test + @DisplayName("1-1. getCacheStatus() — SessionManager.getCacheStatus() 위임") + void testGetCacheStatus_delegates() { + when(mockSessionManager.getCacheStatus()).thenReturn(""); + + assertEquals("", service.getCacheStatus()); + verify(mockSessionManager).getCacheStatus(); + } + + @Test + @DisplayName("1-2. getMasterInstId() — SessionManager.getMasterInstId() 위임") + void testGetMasterInstId_delegates() { + when(mockSessionManager.getMasterInstId()).thenReturn("SERVER01"); + + assertEquals("SERVER01", service.getMasterInstId()); + } + + @Test + @DisplayName("1-3. getLoginCache() — 위임") + void testGetLoginCache_delegates() { + CacheInfoVO vo = makeCacheInfo("LoginCache", 2); + when(mockSessionManager.getLoginCache()).thenReturn(vo); + + assertSame(vo, service.getLoginCache()); + } + + @Test + @DisplayName("1-4. getHttpLoginCache() — 위임") + void testGetHttpLoginCache_delegates() { + CacheInfoVO vo = makeCacheInfo("HttpLoginCache", 1); + when(mockSessionManager.getHttpLoginCache()).thenReturn(vo); + + assertSame(vo, service.getHttpLoginCache()); + } + + @Test + @DisplayName("1-5. getEvictMasterCache() — 위임") + void testGetEvictMasterCache_delegates() { + CacheInfoVO vo = makeCacheInfo("EvictMasterCache", 1); + when(mockSessionManager.getEvictMasterCache()).thenReturn(vo); + + assertSame(vo, service.getEvictMasterCache()); + } + + @Test + @DisplayName("1-6. getTerminalCache() — 위임") + void testGetTerminalCache_delegates() { + CacheInfoVO vo = makeCacheInfo("TerminalCache", 5); + when(mockSessionManager.getTerminalCache()).thenReturn(vo); + + assertSame(vo, service.getTerminalCache()); + } + + @Test + @DisplayName("1-7. getConvertUserIdCache() — 위임") + void testGetConvertUserIdCache_delegates() { + CacheInfoVO vo = makeCacheInfo("ConvertUserIdCache", 3); + when(mockSessionManager.getConvertUserIdCache()).thenReturn(vo); + + assertSame(vo, service.getConvertUserIdCache()); + } + + @Test + @DisplayName("1-8. getSocketCache() — 위임") + void testGetSocketCache_delegates() { + CacheInfoVO vo = makeCacheInfo("SocketCache", 4); + when(mockSessionManager.getSocketCache()).thenReturn(vo); + + assertSame(vo, service.getSocketCache()); + } + + @Test + @DisplayName("1-9. getOutboundAccessTokenCache() — 위임") + void testGetOutboundAccessTokenCache_delegates() { + CacheInfoVO vo = makeCacheInfo("OutBoundAccessToken", 7); + when(mockSessionManager.getOutboundAccessTokenCache()).thenReturn(vo); + + assertSame(vo, service.getOutboundAccessTokenCache()); + } + + @Test + @DisplayName("1-10. getOutboundAccessTokenCache() — 미지원 백엔드는 예외 그대로 전파") + void testGetOutboundAccessTokenCache_unsupported_throwsException() { + when(mockSessionManager.getOutboundAccessTokenCache()).thenThrow(new UnsupportedOperationException()); + + assertThrows(UnsupportedOperationException.class, service::getOutboundAccessTokenCache); + } + + // ========================================================================= + // 2. getAllCaches() 집계 + // ========================================================================= + + @Test + @DisplayName("2-1. getAllCaches() — OutboundAccessToken 포함 7개 캐시 반환") + void testGetAllCaches_outboundSupported_returnsSevenCaches() { + when(mockSessionManager.getLoginCache()).thenReturn(makeCacheInfo("LoginCache", 1)); + when(mockSessionManager.getHttpLoginCache()).thenReturn(makeCacheInfo("HttpLoginCache", 1)); + when(mockSessionManager.getEvictMasterCache()).thenReturn(makeCacheInfo("EvictMasterCache", 1)); + when(mockSessionManager.getTerminalCache()).thenReturn(makeCacheInfo("TerminalCache", 1)); + when(mockSessionManager.getConvertUserIdCache()).thenReturn(makeCacheInfo("ConvertUserIdCache", 1)); + when(mockSessionManager.getSocketCache()).thenReturn(makeCacheInfo("SocketCache", 1)); + when(mockSessionManager.getOutboundAccessTokenCache()).thenReturn(makeCacheInfo("OutBoundAccessToken", 1)); + + List all = service.getAllCaches(); + + assertEquals(7, all.size()); + assertTrue(all.stream().anyMatch(c -> "OutBoundAccessToken".equals(c.getName()))); + } + + @Test + @DisplayName("2-2. getAllCaches() — OutboundAccessToken 미지원(Ehcache)이면 예외 없이 6개만 반환") + void testGetAllCaches_outboundUnsupported_excludedGracefully() { + when(mockSessionManager.getLoginCache()).thenReturn(makeCacheInfo("LoginCache", 1)); + when(mockSessionManager.getHttpLoginCache()).thenReturn(makeCacheInfo("HttpLoginCache", 1)); + when(mockSessionManager.getEvictMasterCache()).thenReturn(makeCacheInfo("EvictMasterCache", 1)); + when(mockSessionManager.getTerminalCache()).thenReturn(makeCacheInfo("TerminalCache", 1)); + when(mockSessionManager.getConvertUserIdCache()).thenReturn(makeCacheInfo("ConvertUserIdCache", 1)); + when(mockSessionManager.getSocketCache()).thenReturn(makeCacheInfo("SocketCache", 1)); + when(mockSessionManager.getOutboundAccessTokenCache()).thenThrow(new UnsupportedOperationException()); + + List all = assertDoesNotThrow(service::getAllCaches); + + assertEquals(6, all.size()); + assertTrue(all.stream().noneMatch(c -> "OutBoundAccessToken".equals(c.getName()))); + } +}