Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/api/healthCheckDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ export const GS25_CLOUDFRONT_403_PATTERNS = [
'403 ERROR',
];

export const EMART24_UPSTREAM_403_PATTERNS = [
'403 Forbidden',
'<title>403 Forbidden</title>',
];

export const HEALTH_CHECKS: HealthCheckDefinition[] = [
{
id: 'cli.contract',
Expand Down Expand Up @@ -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',
Expand Down
14 changes: 12 additions & 2 deletions src/api/healthChecks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<HealthCheckDefinition, 'timeoutMs'>, timeoutMs: number): number {
Expand Down
72 changes: 72 additions & 0 deletions tests/app/app-health-checks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 - <!DOCTYPE html><title>403 Forbidden</title>' },
},
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 - <!DOCTYPE html><title>403 Forbidden</title>' },
},
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({
Expand Down
Loading