From b391e03626ead2a338f713485fd7803809e01687 Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Thu, 11 Jun 2026 20:00:55 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=9D=B4=EB=A7=88=ED=8A=B824=20?= =?UTF-8?q?=ED=97=AC=EC=8A=A4=EC=B2=B4=ED=81=AC=20403=20=EC=95=8C=EB=9E=8C?= =?UTF-8?q?=20=EC=99=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/healthCheckDefinitions.ts | 6 +++ src/api/healthChecks.ts | 14 +++++- tests/app/app-health-checks.test.ts | 72 +++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/api/healthCheckDefinitions.ts b/src/api/healthCheckDefinitions.ts index aef3248..b545c66 100644 --- a/src/api/healthCheckDefinitions.ts +++ b/src/api/healthCheckDefinitions.ts @@ -10,6 +10,11 @@ export const GS25_CLOUDFRONT_403_PATTERNS = [ '403 ERROR', ]; +export const EMART24_UPSTREAM_403_PATTERNS = [ + '403 Forbidden', + '403 Forbidden', +]; + export const HEALTH_CHECKS: HealthCheckDefinition[] = [ { id: 'cli.contract', @@ -45,6 +50,7 @@ export const HEALTH_CHECKS: HealthCheckDefinition[] = [ path: '/api/emart24/products?keyword=%EC%BB%A4%ED%94%BC&pageSize=1', collectionKey: 'products', requiredFields: ['pluCd', 'goodsName', 'itemName', 'name'], + degradedFailurePatterns: EMART24_UPSTREAM_403_PATTERNS, }, { id: 'gs25.products', diff --git a/src/api/healthChecks.ts b/src/api/healthChecks.ts index dac523e..e3f06c5 100644 --- a/src/api/healthChecks.ts +++ b/src/api/healthChecks.ts @@ -2,7 +2,11 @@ * 개별 서비스 헬스 체크 실행기 */ -import { GS25_CLOUDFRONT_403_PATTERNS, HEALTH_CHECKS } from './healthCheckDefinitions.js'; +import { + EMART24_UPSTREAM_403_PATTERNS, + GS25_CLOUDFRONT_403_PATTERNS, + HEALTH_CHECKS, +} from './healthCheckDefinitions.js'; import { hasRequiredRepresentativeFields, toCount, toFirstName } from './healthCheckShape.js'; import type { HealthCheckDefinition, @@ -129,7 +133,13 @@ function shouldDegradeFailedResponse(check: HealthCheckDefinition, message: stri } function shouldDegradeCliContractPath(path: string, message: string): boolean { - return path.startsWith('/api/gs25/') && GS25_CLOUDFRONT_403_PATTERNS.some((pattern) => message.includes(pattern)); + if (path.startsWith('/api/gs25/')) { + return GS25_CLOUDFRONT_403_PATTERNS.some((pattern) => message.includes(pattern)); + } + if (path.startsWith('/api/emart24/')) { + return EMART24_UPSTREAM_403_PATTERNS.some((pattern) => message.includes(pattern)); + } + return false; } function resolveCheckTimeoutMs(check: Pick, timeoutMs: number): number { diff --git a/tests/app/app-health-checks.test.ts b/tests/app/app-health-checks.test.ts index 9221dc5..570b4a5 100644 --- a/tests/app/app-health-checks.test.ts +++ b/tests/app/app-health-checks.test.ts @@ -216,6 +216,78 @@ describe('GET /api/health/checks', () => { ); }); + it('이마트24 상품 upstream 403은 degraded로 집계한다', async () => { + mockFetch.mockResolvedValueOnce( + jsonResponse( + { + success: false, + error: { message: 'API 요청 실패: 403 Forbidden - 403 Forbidden' }, + }, + 502, + ), + ); + + const res = await app.request( + '/api/health/checks?check=emart24.products&fresh=true&transport=network', + { + headers: { Authorization: 'Bearer test-secret' }, + }, + { + HEALTH_CHECK_SECRET: 'test-secret', + }, + ); + + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.status).toBe('degraded'); + expect(data.checks[0]).toEqual( + expect.objectContaining({ + id: 'emart24.products', + status: 'degraded', + message: expect.stringContaining('403 Forbidden'), + }), + ); + }); + + it('CLI 계약 체크에서 이마트24 upstream 403은 degraded로 집계한다', async () => { + mockFetch.mockImplementation((input: RequestInfo | URL) => + Promise.resolve( + String(input).includes('/api/emart24/products') + ? jsonResponse( + { + success: false, + error: { message: 'API 요청 실패: 403 Forbidden - 403 Forbidden' }, + }, + 502, + ) + : String(input).includes('/health') + ? jsonResponse({ status: 'ok' }) + : jsonResponse({ success: true, data: { products: [{ name: '상품' }] }, meta: { total: 1 } }), + ), + ); + + const res = await app.request( + '/api/health/checks?check=cli.contract&mode=deep&fresh=true&transport=network', + { + headers: { Authorization: 'Bearer test-secret' }, + }, + { + HEALTH_CHECK_SECRET: 'test-secret', + }, + ); + + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.status).toBe('degraded'); + expect(data.checks[0]).toEqual( + expect.objectContaining({ + id: 'cli.contract', + status: 'degraded', + message: expect.stringContaining('/api/emart24/products'), + }), + ); + }); + it('fresh가 아니면 동일한 체크 결과를 캐시한다', async () => { mockFetch.mockResolvedValue( jsonResponse({