From 7f26ec29ff4515f228c9c28212913fd09649bf93 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:07:19 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20testing=20improvement:=20Add=20t?= =?UTF-8?q?ests=20for=20api-client.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/package.json | 1 + packages/cli/src/lib/api-client.test.ts | 91 +++++++++++++ pnpm-lock.yaml | 167 +++++++++++++++++++++++- 3 files changed, 253 insertions(+), 6 deletions(-) create mode 100644 packages/cli/src/lib/api-client.test.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 5b33992..014c7cf 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -28,6 +28,7 @@ "devDependencies": { "@argos/shared": "workspace:*", "@types/node": "^20", + "msw": "^2.14.6", "typescript": "^5", "vitest": "^2.1.9" }, diff --git a/packages/cli/src/lib/api-client.test.ts b/packages/cli/src/lib/api-client.test.ts new file mode 100644 index 0000000..6251850 --- /dev/null +++ b/packages/cli/src/lib/api-client.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect, beforeAll, afterEach, afterAll, vi } from 'vitest' +import { apiRequest } from './api-client.js' +import { setupServer } from 'msw/node' +import { http, HttpResponse } from 'msw' + +const server = setupServer( + http.get('https://api.example.com/test', () => { + return HttpResponse.json({ success: true, message: 'Hello' }) + }), + http.post('https://api.example.com/test-auth', ({ request }) => { + const authHeader = request.headers.get('Authorization') + if (authHeader === 'Bearer test-token') { + return HttpResponse.json({ authenticated: true }) + } + return new HttpResponse('Unauthorized', { status: 401 }) + }), + http.get('https://api.example.com/error-json', () => { + return HttpResponse.json({ error: { message: 'Custom JSON Error' } }, { status: 400 }) + }), + http.get('https://api.example.com/error-text', () => { + return new HttpResponse('Custom Text Error', { status: 400 }) + }), + http.get('https://api.example.com/network-error', () => { + return HttpResponse.error() + }) +) + +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) +afterEach(() => { + server.resetHandlers() + vi.clearAllMocks() +}) +afterAll(() => server.close()) + +describe('apiRequest', () => { + it('makes a successful request with base URL', async () => { + const result = await apiRequest<{ success: boolean; message: string }>('/test', { + baseUrl: 'https://api.example.com', + }) + expect(result).toEqual({ success: true, message: 'Hello' }) + }) + + it('includes auth token and custom headers', async () => { + const result = await apiRequest<{ authenticated: boolean }>('/test-auth', { + baseUrl: 'https://api.example.com', + method: 'POST', + token: 'test-token', + headers: { + 'X-Custom-Header': 'CustomValue', + }, + }) + expect(result).toEqual({ authenticated: true }) + }) + + it('handles JSON errors correctly', async () => { + await expect( + apiRequest('/error-json', { baseUrl: 'https://api.example.com' }) + ).rejects.toThrow('API Error (400): Custom JSON Error') + }) + + it('handles text errors correctly', async () => { + await expect( + apiRequest('/error-text', { baseUrl: 'https://api.example.com' }) + ).rejects.toThrow('API Error (400): Custom Text Error') + }) + + it('handles network errors', async () => { + await expect( + apiRequest('/network-error', { baseUrl: 'https://api.example.com' }) + ).rejects.toThrow('Failed to fetch') + }) + + it('handles timeout correctly', async () => { + // Override global fetch completely to simulate timeout scenario deterministically + const originalFetch = global.fetch + + // Simulate a fetch that triggers an AbortError synchronously to avoid unhandled async rejection + // when using fake timers with native fetch. We just inject the abort error. + global.fetch = vi.fn().mockImplementation(() => { + const err = new Error('The operation was aborted') + err.name = 'AbortError' + return Promise.reject(err) + }) + + const reqPromise = apiRequest('/timeout', { baseUrl: 'https://api.example.com' }) + + await expect(reqPromise).rejects.toThrow('API request timed out') + + global.fetch = originalFetch + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6ff063..c17ec6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,12 +42,15 @@ importers: '@types/node': specifier: ^20 version: 20.19.39 + msw: + specifier: ^2.14.6 + version: 2.14.6(@types/node@20.19.39)(typescript@5.9.3) typescript: specifier: ^5 version: 5.9.3 vitest: specifier: ^2.1.9 - version: 2.1.9(@types/node@20.19.39)(lightningcss@1.32.0)(msw@2.13.2(@types/node@20.19.39)(typescript@5.9.3)) + version: 2.1.9(@types/node@20.19.39)(lightningcss@1.32.0)(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3)) packages/shared: dependencies: @@ -163,7 +166,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.9 - version: 2.1.9(@types/node@20.19.39)(lightningcss@1.32.0)(msw@2.13.2(@types/node@20.19.39)(typescript@5.9.3)) + version: 2.1.9(@types/node@20.19.39)(lightningcss@1.32.0)(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3)) packages: @@ -717,6 +720,10 @@ packages: resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} engines: {node: '>=18'} + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/checkbox@4.3.2': resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} engines: {node: '>=18'} @@ -735,6 +742,15 @@ packages: '@types/node': optional: true + '@inquirer/confirm@6.1.1': + resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/core@10.3.2': resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} engines: {node: '>=18'} @@ -744,6 +760,15 @@ packages: '@types/node': optional: true + '@inquirer/core@11.2.1': + resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/editor@4.2.23': resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} engines: {node: '>=18'} @@ -775,6 +800,10 @@ packages: resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} engines: {node: '>=18'} + '@inquirer/figures@2.0.7': + resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/input@4.3.1': resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} engines: {node: '>=18'} @@ -847,6 +876,15 @@ packages: '@types/node': optional: true + '@inquirer/type@4.0.7': + resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -965,6 +1003,9 @@ packages: '@open-draft/deferred-promise@2.2.0': resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + '@open-draft/deferred-promise@3.0.0': + resolution: {integrity: sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==} + '@open-draft/logger@0.3.0': resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} @@ -1342,6 +1383,9 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} @@ -2352,9 +2396,18 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -2550,6 +2603,9 @@ packages: headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + headers-polyfill@5.0.1: + resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} @@ -3193,10 +3249,24 @@ packages: typescript: optional: true + msw@2.14.6: + resolution: {integrity: sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3639,6 +3709,9 @@ packages: rettime@0.10.1: resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} + rettime@0.11.11: + resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3702,6 +3775,9 @@ packages: server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -4766,6 +4842,8 @@ snapshots: '@inquirer/ansi@1.0.2': {} + '@inquirer/ansi@2.0.7': {} + '@inquirer/checkbox@4.3.2(@types/node@20.19.39)': dependencies: '@inquirer/ansi': 1.0.2 @@ -4783,6 +4861,13 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 + '@inquirer/confirm@6.1.1(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@20.19.39) + '@inquirer/type': 4.0.7(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + '@inquirer/core@10.3.2(@types/node@20.19.39)': dependencies: '@inquirer/ansi': 1.0.2 @@ -4796,6 +4881,18 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 + '@inquirer/core@11.2.1(@types/node@20.19.39)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@20.19.39) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 20.19.39 + '@inquirer/editor@4.2.23(@types/node@20.19.39)': dependencies: '@inquirer/core': 10.3.2(@types/node@20.19.39) @@ -4821,6 +4918,8 @@ snapshots: '@inquirer/figures@1.0.15': {} + '@inquirer/figures@2.0.7': {} + '@inquirer/input@4.3.1(@types/node@20.19.39)': dependencies: '@inquirer/core': 10.3.2(@types/node@20.19.39) @@ -4889,6 +4988,10 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 + '@inquirer/type@4.0.7(@types/node@20.19.39)': + optionalDependencies: + '@types/node': 20.19.39 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5000,6 +5103,8 @@ snapshots: '@open-draft/deferred-promise@2.2.0': {} + '@open-draft/deferred-promise@3.0.0': {} + '@open-draft/logger@0.3.0': dependencies: is-node-process: 1.2.0 @@ -5298,6 +5403,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/set-cookie-parser@2.4.10': + dependencies: + '@types/node': 20.19.39 + '@types/statuses@2.0.6': {} '@types/unist@2.0.11': {} @@ -5465,13 +5574,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.9(msw@2.13.2(@types/node@20.19.39)(typescript@5.9.3))(vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0))': + '@vitest/mocker@2.1.9(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0))': dependencies: '@vitest/spy': 2.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - msw: 2.13.2(@types/node@20.19.39)(typescript@5.9.3) + msw: 2.14.6(@types/node@20.19.39)(typescript@5.9.3) vite: 5.4.21(@types/node@20.19.39)(lightningcss@1.32.0) '@vitest/pretty-format@2.1.9': @@ -6473,8 +6582,18 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + fast-uri@3.1.0: {} + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -6684,6 +6803,11 @@ snapshots: headers-polyfill@4.0.3: {} + headers-polyfill@5.0.1: + dependencies: + '@types/set-cookie-parser': 2.4.10 + set-cookie-parser: 3.1.0 + hermes-estree@0.25.1: {} hermes-parser@0.25.1: @@ -7474,8 +7598,35 @@ snapshots: transitivePeerDependencies: - '@types/node' + msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 6.1.1(@types/node@20.19.39) + '@mswjs/interceptors': 0.41.3 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.13.2 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.11 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.1 + type-fest: 5.5.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + mute-stream@2.0.0: {} + mute-stream@3.0.0: {} + nanoid@3.3.11: {} nanoid@3.3.12: {} @@ -7962,6 +8113,8 @@ snapshots: rettime@0.10.1: {} + rettime@0.11.11: {} + reusify@1.1.0: {} rollup@4.60.1: @@ -8067,6 +8220,8 @@ snapshots: server-only@0.0.1: {} + set-cookie-parser@3.1.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -8632,10 +8787,10 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.32.0 - vitest@2.1.9(@types/node@20.19.39)(lightningcss@1.32.0)(msw@2.13.2(@types/node@20.19.39)(typescript@5.9.3)): + vitest@2.1.9(@types/node@20.19.39)(lightningcss@1.32.0)(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3)): dependencies: '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(msw@2.13.2(@types/node@20.19.39)(typescript@5.9.3))(vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0)) + '@vitest/mocker': 2.1.9(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9