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
1 change: 1 addition & 0 deletions src/api/healthCheckDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export const HEALTH_CHECKS: HealthCheckDefinition[] = [
path: '/api/emart24/inventory?keyword=%EC%BB%A4%ED%94%BC&storeKeyword=%EA%B0%95%EB%82%A8&limit=1',
collectionKey: 'inventoryItems',
requiredFields: ['pluCd', 'goodsName', 'itemName', 'name'],
degradedFailurePatterns: EMART24_UPSTREAM_403_PATTERNS,
},
{
id: 'gs25.inventory',
Expand Down
14 changes: 9 additions & 5 deletions src/services/cgv/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ interface CommonFetchParams {
const DEFAULT_THEATER_CODE = '0056';
const MAX_FALLBACK_THEATERS = 5;

function asArray<T>(value: T[] | null | undefined): T[] {
return Array.isArray(value) ? value : [];
}

async function resolveTheaterCode(playDate: string, theaterCode: string | undefined, params: CommonFetchParams) {
if (theaterCode) {
return theaterCode;
Expand Down Expand Up @@ -59,7 +63,7 @@ async function fetchMoviesByTheaterCode(
params.zyteApiKey,
);

return (response.data || [])
return asArray(response.data)
.filter((item) => item.movNo && item.movNm)
.map((item) => ({
movieCode: item.movNo as string,
Expand Down Expand Up @@ -89,7 +93,7 @@ async function fetchTimetableByMovieCode(
params.zyteApiKey,
);

return (response.data || [])
return asArray(response.data)
.filter((item) => item.siteNo && item.movNo && item.scnYmd)
.map((item) => ({
scheduleId: `${item.scnYmd}${item.siteNo}${item.scnSseq || ''}`,
Expand Down Expand Up @@ -124,7 +128,7 @@ async function fetchTimetableBySite(
params.zyteApiKey,
);

return (response.data || [])
return asArray(response.data)
.filter((item) => item.siteNo && item.scnYmd)
.map((item) => ({
scheduleId: `${item.scnYmd}${item.siteNo}${item.scnSseq || ''}`,
Expand Down Expand Up @@ -152,8 +156,8 @@ export async function fetchCgvTheaters(params: CommonFetchParams): Promise<CgvTh
params.zyteApiKey,
);

const list = (response.data || []).flatMap((region) =>
(region.siteList || []).map((site) => ({
const list = asArray(response.data).flatMap((region) =>
asArray(region.siteList).map((site) => ({
theaterCode: site.siteNo || '',
theaterName: site.siteNm || '',
regionCode: region.regnGrpCd || undefined,
Expand Down
33 changes: 33 additions & 0 deletions tests/app/app-health-checks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,39 @@ 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.inventory&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: 'emart24.inventory',
status: 'degraded',
message: expect.stringContaining('403 Forbidden'),
}),
);
});

it('CLI 계약 체크에서 이마트24 upstream 403은 degraded로 집계한다', async () => {
mockFetch.mockImplementation((input: RequestInfo | URL) =>
Promise.resolve(
Expand Down
56 changes: 56 additions & 0 deletions tests/services/cgv/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,20 @@ describe('fetchCgvTheaters', () => {
const result = await fetchCgvTheaters({});
expect(result).toEqual([]);
});

it('data가 배열이 아니면 빈 배열을 반환한다', async () => {
mockFetch.mockResolvedValue(
new Response(
JSON.stringify({
statusCode: 0,
data: { status: 'temporarily-changed' },
}),
),
);

const result = await fetchCgvTheaters({});
expect(result).toEqual([]);
});
});

describe('fetchCgvMovies', () => {
Expand Down Expand Up @@ -239,6 +253,20 @@ describe('fetchCgvMovies', () => {
const result = await fetchCgvMovies({ playDate: '20260304', theaterCode: '0056' });
expect(result).toEqual([]);
});

it('영화 목록 data가 배열이 아니면 빈 배열을 반환한다', async () => {
mockFetch.mockResolvedValue(
new Response(
JSON.stringify({
statusCode: 0,
data: { status: 'temporarily-changed' },
}),
),
);

const result = await fetchCgvMovies({ playDate: '20260304', theaterCode: '0056' });
expect(result).toEqual([]);
});
});

describe('fetchCgvTimetable', () => {
Expand Down Expand Up @@ -660,6 +688,34 @@ describe('fetchCgvTimetable', () => {
expect(result).toEqual([]);
});

it('시간표 data가 배열이 아니면 빈 배열로 처리 후 fallback한다', async () => {
mockFetch
.mockResolvedValueOnce(
new Response(
JSON.stringify({
statusCode: 0,
data: { status: 'temporarily-changed' },
}),
),
)
.mockResolvedValueOnce(
new Response(
JSON.stringify({
statusCode: 0,
data: { status: 'temporarily-changed' },
}),
),
);

const result = await fetchCgvTimetable({
playDate: '20260304',
theaterCode: '0056',
movieCode: '30000985',
});

expect(result).toEqual([]);
});

it('movieCode가 있고 사이트 시간표가 비면 영화코드 조회로 fallback한다', async () => {
mockFetch
.mockResolvedValueOnce(
Expand Down