From 9fdd8e233b37a665aab4976df8a62d2310ee6c8f Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Mon, 27 Jul 2026 21:56:48 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Zyte=20=ED=8F=89=EB=AC=B8=20520=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/configStatus.ts | 2 +- src/utils/zyte.ts | 12 +++++++++++- tests/app/app-info-pages.test.ts | 5 ++++- tests/services/cu/client.test.ts | 20 ++++++++++++++++++++ tests/services/oliveyoung/client.test.ts | 10 ++++++---- tests/utils/zyte.test.ts | 14 ++++++++++++++ 6 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/api/configStatus.ts b/src/api/configStatus.ts index 4d717d5..5dbff74 100644 --- a/src/api/configStatus.ts +++ b/src/api/configStatus.ts @@ -26,7 +26,7 @@ export function buildConfigStatus(bindings?: AppBindings): ConfigStatus { }, zyteApiKey: { configured: isConfigured(bindings?.ZYTE_API_KEY), - usedBy: ['oliveyoung', 'gs25', 'lottemart', 'cgv'], + usedBy: ['oliveyoung', 'gs25', 'cu', 'seveneleven', 'lottemart', 'cgv'], }, naverLocalSearch: { configured: diff --git a/src/utils/zyte.ts b/src/utils/zyte.ts index 9074baf..7453e12 100644 --- a/src/utils/zyte.ts +++ b/src/utils/zyte.ts @@ -85,6 +85,16 @@ function isRetryableError(error: unknown): boolean { return isAbortError(error) || error instanceof RetryableZyteError; } +function parseZyteResponseBody(text: string): ZyteExtractResponse { + try { + return JSON.parse(text) as ZyteExtractResponse; + } catch { + return { + detail: text.trim().replace(/\s+/g, ' ').slice(0, 300), + }; + } +} + function wait(ms: number): Promise { if (ms <= 0) { return Promise.resolve(); @@ -139,7 +149,7 @@ export async function requestByZyte(options: ZyteExtractOptions): Promise { expect(data.status).toBe('ok'); expect(data.config).toEqual({ googleMapsApiKey: { configured: true, usedBy: expect.arrayContaining(['gs25', 'cgv']) }, - zyteApiKey: { configured: false, usedBy: expect.arrayContaining(['oliveyoung', 'cgv']) }, + zyteApiKey: { + configured: false, + usedBy: expect.arrayContaining(['oliveyoung', 'cgv', 'cu', 'seveneleven']), + }, naverLocalSearch: { configured: false, usedBy: ['places'] }, opinetApiKey: { configured: false, usedBy: ['opinet'] }, supabaseFeedback: { configured: false, usedBy: ['feedback'] }, diff --git a/tests/services/cu/client.test.ts b/tests/services/cu/client.test.ts index 061c7de..097ee2f 100644 --- a/tests/services/cu/client.test.ts +++ b/tests/services/cu/client.test.ts @@ -504,6 +504,26 @@ describe('fetchCuStock', () => { }); }); + it('Zyte가 평문 HTTP 520을 반환해도 재고를 unavailable로 반환한다', async () => { + mockFetch + .mockResolvedValueOnce(new Response(JSON.stringify({ areaList: [] }))) + .mockResolvedValueOnce(new Response('blocked', { status: 403 })) + .mockResolvedValueOnce(new Response('error code: 520', { status: 520 })); + + const result = await fetchCuStock( + { keyword: '과자', limit: 1, offset: 0, searchSort: 'recom' }, + { apiKey: 'test-key' }, + ); + + expect(result).toEqual({ + available: false, + unavailableReason: expect.stringContaining('Website Ban'), + totalCount: 0, + spellModifyYn: 'N', + items: [], + }); + }); + it('Zyte 키 없이 원본 재고가 차단되어도 unavailable로 반환한다', async () => { mockFetch .mockResolvedValueOnce(new Response(JSON.stringify({ areaList: [] }))) diff --git a/tests/services/oliveyoung/client.test.ts b/tests/services/oliveyoung/client.test.ts index 71870db..672ef49 100644 --- a/tests/services/oliveyoung/client.test.ts +++ b/tests/services/oliveyoung/client.test.ts @@ -19,6 +19,7 @@ beforeEach(() => { }); afterEach(() => { + vi.unstubAllGlobals(); vi.restoreAllMocks(); delete process.env.ZYTE_API_KEY; }); @@ -600,10 +601,11 @@ describe('fetchOliveyoungProducts', () => { mockFetch.mockResolvedValue({ ok: true, status: 200, - json: async () => ({ - statusCode: 200, - httpResponseBody: encodedBody, - }), + text: async () => + JSON.stringify({ + statusCode: 200, + httpResponseBody: encodedBody, + }), }); await expect( diff --git a/tests/utils/zyte.test.ts b/tests/utils/zyte.test.ts index 8b0d9aa..bd3626c 100644 --- a/tests/utils/zyte.test.ts +++ b/tests/utils/zyte.test.ts @@ -62,6 +62,20 @@ describe('requestByZyte', () => { expect(mockFetch).toHaveBeenCalledTimes(2); }); + it('Zyte API가 평문 520 오류를 반환해도 상태와 본문을 보존한다', async () => { + mockFetch.mockResolvedValueOnce(new Response('error code: 520', { status: 520 })); + + await expect( + requestByZyte({ + apiKey: 'test-key', + url: 'https://example.com/api', + retries: 0, + }), + ).rejects.toThrow('Zyte API 호출 실패: 520 error code: 520'); + + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + it('대상 사이트 5xx 응답이면 한 번 재시도한다', async () => { mockFetch .mockResolvedValueOnce(new Response(JSON.stringify({ statusCode: 520, httpResponseBody: 'e30=' })))