From d905b7598dd6f8994af0a32a5e0b5b814b11ad1b Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 12 Mar 2026 15:27:09 +0200 Subject: [PATCH] refactor: refresh test suite to be modernised (#7489) * chore: port karma tests * chore: port karma tests * chore: port karma tests * chore: tests * chore: tests * chore: tests * chore: fix issues with port collisions * refactor: utils tests * refactor: utils tests * refactor: utils tests * refactor: tests to vitests * refactor: tests to vitests * refactor: tests to vitests * refactor: tests to vitests * refactor: tests to vitests * refactor: tests to vitests * refactor: tests to vitests * refactor: ci * chore: install pw deps * chore: fixx ai feedback * chore: wip compatability tests * chore: wip compatability tests * chore: wip compatability tests * refactor: wip smoke * chore: smoke test run * chore: update unzip * chore: update testing * chore: update testing * chore: update testing * chore: update testing * chore: update testing * chore: skip tests that cannot run on node 16 and lower * chore: fix 16x under tests * chore: rest of tests * fix: functions and runs * feat: added tests for esm smoke * feat: added smoke * chore: ignore ai gen plans * chore: ci fixes * chore: fix small p2s --- .github/workflows/run-ci.yml | 99 +- .gitignore | 3 +- package.json | 1 + test/specs/core/enhanceError.spec.js | 0 tests/browser/adapter.browser.test.js | 178 ++ tests/browser/basicAuth.browser.test.js | 136 ++ tests/browser/cancel.browser.test.js | 173 ++ tests/browser/cancelToken.browser.test.js | 90 + tests/browser/cookies.browser.test.js | 56 + tests/browser/defaults.browser.test.js | 282 +++ tests/browser/formdata.browser.test.js | 104 ++ tests/browser/headers.browser.test.js | 228 +++ tests/browser/instance.browser.test.js | 255 +++ tests/browser/interceptors.browser.test.js | 745 ++++++++ tests/browser/isURLSameOrigin.browser.test.js | 13 + tests/browser/options.browser.test.js | 220 +++ tests/browser/progress.browser.test.js | 230 +++ tests/browser/promise.browser.test.js | 122 ++ tests/browser/requests.browser.test.js | 497 ++++++ tests/browser/settle.browser.test.js | 99 ++ tests/browser/smoke.browser.test.js | 10 - tests/browser/toFormData.browser.test.js | 121 ++ tests/browser/transform.browser.test.js | 265 +++ tests/browser/xsrf.browser.test.js | 182 ++ tests/setup/server.js | 2 - tests/smoke/cjs/package-lock.json | 1305 ++++++++++++++ tests/smoke/cjs/package.json | 19 + tests/smoke/cjs/tests/auth.smoke.test.cjs | 137 ++ tests/smoke/cjs/tests/basic.smoke.test.cjs | 145 ++ tests/smoke/cjs/tests/cancel.smoke.test.cjs | 117 ++ tests/smoke/cjs/tests/error.smoke.test.cjs | 137 ++ tests/smoke/cjs/tests/fetch.smoke.test.cjs | 145 ++ tests/smoke/cjs/tests/files.smoke.test.cjs | 115 ++ tests/smoke/cjs/tests/formData.smoke.test.cjs | 110 ++ tests/smoke/cjs/tests/headers.smoke.test.cjs | 125 ++ tests/smoke/cjs/tests/http2.smoke.test.cjs | 86 + tests/smoke/cjs/tests/instance.smoke.test.cjs | 146 ++ .../cjs/tests/interceptors.smoke.test.cjs | 149 ++ tests/smoke/cjs/tests/progress.smoke.test.cjs | 113 ++ .../smoke/cjs/tests/rateLimit.smoke.test.cjs | 105 ++ tests/smoke/cjs/tests/timeout.smoke.test.cjs | 116 ++ .../smoke/cjs/tests/urlencode.smoke.test.cjs | 146 ++ tests/smoke/esm/package-lock.json | 1549 +++++++++++++++++ tests/smoke/esm/package.json | 18 + tests/smoke/esm/tests/auth.smoke.test.js | 137 ++ tests/smoke/esm/tests/basic.smoke.test.js | 145 ++ tests/smoke/esm/tests/cancel.smoke.test.js | 111 ++ tests/smoke/esm/tests/error.smoke.test.js | 139 ++ tests/smoke/esm/tests/fetch.smoke.test.js | 142 ++ tests/smoke/esm/tests/files.smoke.test.js | 115 ++ tests/smoke/esm/tests/formData.smoke.test.js | 107 ++ tests/smoke/esm/tests/headers.smoke.test.js | 125 ++ tests/smoke/esm/tests/http2.smoke.test.js | 86 + tests/smoke/esm/tests/instance.smoke.test.js | 146 ++ .../esm/tests/interceptors.smoke.test.js | 149 ++ tests/smoke/esm/tests/progress.smoke.test.js | 113 ++ tests/smoke/esm/tests/rateLimit.smoke.test.js | 105 ++ tests/smoke/esm/tests/timeout.smoke.test.js | 116 ++ tests/smoke/esm/tests/urlencode.smoke.test.js | 146 ++ tests/smoke/esm/vitest.config.js | 17 + ...r-details.test.js => errorDetails.test.js} | 0 tests/unit/adapters/fetch.test.js | 662 ++++--- tests/unit/adapters/http.test.js | 308 ++-- tests/unit/api.test.js | 97 ++ tests/unit/cancel/canceledError.test.js | 23 + tests/unit/cancel/isCancel.test.js | 13 + tests/unit/core/AxiosError.test.js | 102 ++ tests/unit/core/buildFullPath.test.js | 34 + tests/unit/core/mergeConfig.test.js | 357 ++++ tests/unit/core/transformData.test.js | 49 + tests/unit/helpers/bind.test.js | 13 + tests/unit/helpers/buildURL.test.js | 126 ++ tests/unit/helpers/combineURLs.test.js | 24 + tests/unit/helpers/formDataToJSON.test.js | 72 + tests/unit/helpers/isAbsoluteURL.test.js | 24 + tests/unit/helpers/isAxiosError.test.js | 21 + tests/unit/helpers/parseHeaders.test.js | 43 + tests/unit/helpers/spread.test.js | 21 + tests/unit/helpers/validator.test.js | 67 + tests/unit/utils/endsWith.test.js | 13 + tests/unit/utils/extend.test.js | 40 + tests/unit/utils/forEach.test.js | 69 + tests/unit/utils/isX.test.js | 80 + tests/unit/utils/kindOf.test.js | 13 + tests/unit/utils/kindOfTest.test.js | 12 + tests/unit/utils/merge.test.js | 97 ++ tests/unit/utils/toArray.test.js | 13 + tests/unit/utils/toFlatObject.test.js | 13 + tests/unit/utils/trim.test.js | 12 + vitest.config.js | 16 + 90 files changed, 12796 insertions(+), 431 deletions(-) delete mode 100644 test/specs/core/enhanceError.spec.js create mode 100644 tests/browser/adapter.browser.test.js create mode 100644 tests/browser/basicAuth.browser.test.js create mode 100644 tests/browser/cancel.browser.test.js create mode 100644 tests/browser/cancelToken.browser.test.js create mode 100644 tests/browser/cookies.browser.test.js create mode 100644 tests/browser/defaults.browser.test.js create mode 100644 tests/browser/formdata.browser.test.js create mode 100644 tests/browser/headers.browser.test.js create mode 100644 tests/browser/instance.browser.test.js create mode 100644 tests/browser/interceptors.browser.test.js create mode 100644 tests/browser/isURLSameOrigin.browser.test.js create mode 100644 tests/browser/options.browser.test.js create mode 100644 tests/browser/progress.browser.test.js create mode 100644 tests/browser/promise.browser.test.js create mode 100644 tests/browser/requests.browser.test.js create mode 100644 tests/browser/settle.browser.test.js delete mode 100644 tests/browser/smoke.browser.test.js create mode 100644 tests/browser/toFormData.browser.test.js create mode 100644 tests/browser/transform.browser.test.js create mode 100644 tests/browser/xsrf.browser.test.js create mode 100644 tests/smoke/cjs/package-lock.json create mode 100644 tests/smoke/cjs/package.json create mode 100644 tests/smoke/cjs/tests/auth.smoke.test.cjs create mode 100644 tests/smoke/cjs/tests/basic.smoke.test.cjs create mode 100644 tests/smoke/cjs/tests/cancel.smoke.test.cjs create mode 100644 tests/smoke/cjs/tests/error.smoke.test.cjs create mode 100644 tests/smoke/cjs/tests/fetch.smoke.test.cjs create mode 100644 tests/smoke/cjs/tests/files.smoke.test.cjs create mode 100644 tests/smoke/cjs/tests/formData.smoke.test.cjs create mode 100644 tests/smoke/cjs/tests/headers.smoke.test.cjs create mode 100644 tests/smoke/cjs/tests/http2.smoke.test.cjs create mode 100644 tests/smoke/cjs/tests/instance.smoke.test.cjs create mode 100644 tests/smoke/cjs/tests/interceptors.smoke.test.cjs create mode 100644 tests/smoke/cjs/tests/progress.smoke.test.cjs create mode 100644 tests/smoke/cjs/tests/rateLimit.smoke.test.cjs create mode 100644 tests/smoke/cjs/tests/timeout.smoke.test.cjs create mode 100644 tests/smoke/cjs/tests/urlencode.smoke.test.cjs create mode 100644 tests/smoke/esm/package-lock.json create mode 100644 tests/smoke/esm/package.json create mode 100644 tests/smoke/esm/tests/auth.smoke.test.js create mode 100644 tests/smoke/esm/tests/basic.smoke.test.js create mode 100644 tests/smoke/esm/tests/cancel.smoke.test.js create mode 100644 tests/smoke/esm/tests/error.smoke.test.js create mode 100644 tests/smoke/esm/tests/fetch.smoke.test.js create mode 100644 tests/smoke/esm/tests/files.smoke.test.js create mode 100644 tests/smoke/esm/tests/formData.smoke.test.js create mode 100644 tests/smoke/esm/tests/headers.smoke.test.js create mode 100644 tests/smoke/esm/tests/http2.smoke.test.js create mode 100644 tests/smoke/esm/tests/instance.smoke.test.js create mode 100644 tests/smoke/esm/tests/interceptors.smoke.test.js create mode 100644 tests/smoke/esm/tests/progress.smoke.test.js create mode 100644 tests/smoke/esm/tests/rateLimit.smoke.test.js create mode 100644 tests/smoke/esm/tests/timeout.smoke.test.js create mode 100644 tests/smoke/esm/tests/urlencode.smoke.test.js create mode 100644 tests/smoke/esm/vitest.config.js rename tests/unit/adapters/{error-details.test.js => errorDetails.test.js} (100%) create mode 100644 tests/unit/api.test.js create mode 100644 tests/unit/cancel/canceledError.test.js create mode 100644 tests/unit/cancel/isCancel.test.js create mode 100644 tests/unit/core/AxiosError.test.js create mode 100644 tests/unit/core/buildFullPath.test.js create mode 100644 tests/unit/core/mergeConfig.test.js create mode 100644 tests/unit/core/transformData.test.js create mode 100644 tests/unit/helpers/bind.test.js create mode 100644 tests/unit/helpers/buildURL.test.js create mode 100644 tests/unit/helpers/combineURLs.test.js create mode 100644 tests/unit/helpers/formDataToJSON.test.js create mode 100644 tests/unit/helpers/isAbsoluteURL.test.js create mode 100644 tests/unit/helpers/isAxiosError.test.js create mode 100644 tests/unit/helpers/parseHeaders.test.js create mode 100644 tests/unit/helpers/spread.test.js create mode 100644 tests/unit/helpers/validator.test.js create mode 100644 tests/unit/utils/endsWith.test.js create mode 100644 tests/unit/utils/extend.test.js create mode 100644 tests/unit/utils/forEach.test.js create mode 100644 tests/unit/utils/isX.test.js create mode 100644 tests/unit/utils/kindOf.test.js create mode 100644 tests/unit/utils/kindOfTest.test.js create mode 100644 tests/unit/utils/merge.test.js create mode 100644 tests/unit/utils/toArray.test.js create mode 100644 tests/unit/utils/toFlatObject.test.js create mode 100644 tests/unit/utils/trim.test.js diff --git a/.github/workflows/run-ci.yml b/.github/workflows/run-ci.yml index 6fdf1960..05803bf1 100644 --- a/.github/workflows/run-ci.yml +++ b/.github/workflows/run-ci.yml @@ -15,12 +15,43 @@ concurrency: jobs: ci: runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v6 + with: + persist-credentials: true + - name: Setup node + uses: actions/setup-node@v6 + with: + node-version: 24.x + cache: npm + - name: Install dependencies + run: npm ci + - name: Build project + run: npm run build + - name: Install Playwright with deps + run: npx playwright install --with-deps + - name: Run unit tests + run: npm run test:vitest:unit + - name: Run browser tests + run: npm run test:vitest:browser:headless + - name: Dependency Review + uses: actions/dependency-review-action@v4 + - name: Upload build artifact + uses: actions/upload-artifact@v7 + with: + name: axios + path: dist + retention-days: 1 + cjs-smoke-tests: + name: CJS smoke tests (Node ${{ matrix.node-version }}) + needs: ci + runs-on: ubuntu-latest strategy: - matrix: - node-version: [14.x, 16.x, 18.x, 20.x, 22.x, 24.x] fail-fast: false - + matrix: + node-version: [12, 14, 16, 18] steps: - name: Checkout repo uses: actions/checkout@v6 @@ -31,21 +62,51 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: npm - - name: Install dependencies Node 14 - if: matrix.node-version == '14.x' - run: npm i + cache-dependency-path: tests/smoke/cjs/package-lock.json - name: Install dependencies - if: matrix.node-version != '14.x' + if: matrix.node-version == 16 || matrix.node-version == 18 run: npm ci - - name: Build project - run: npm run build - - name: Run unit tests - run: npm run test:node - - name: Run package tests - run: npm run test:package - - name: Run browser tests - run: npm run test:browser - if: matrix.node-version == '24.x' - - name: Dependency Review - uses: actions/dependency-review-action@v4 - if: matrix.node-version == '24.x' + - name: Download build artifact + uses: actions/download-artifact@v8 + with: + name: axios + path: dist + - name: Install CJS smoke test dependencies + working-directory: tests/smoke/cjs + run: npm install + - name: Run CJS smoke tests + working-directory: tests/smoke/cjs + run: npm run test:smoke:cjs:mocha + + esm-smoke-tests: + name: ESM smoke tests (Node ${{ matrix.node-version }}) + needs: ci + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [20, 22, 24] + steps: + - name: Checkout repo + uses: actions/checkout@v6 + with: + persist-credentials: true + - name: Setup node + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + cache: npm + cache-dependency-path: tests/smoke/esm/package-lock.json + - name: Install dependencies + run: npm ci + - name: Download build artifact + uses: actions/download-artifact@v8 + with: + name: axios + path: dist + - name: Install ESM smoke test dependencies + working-directory: tests/smoke/esm + run: npm install + - name: Run ESM smoke tests + working-directory: tests/smoke/esm + run: npm run test:smoke:esm:vitest diff --git a/.gitignore b/.gitignore index ea8c3b7e..bcca6425 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ backup/ .npmrc .env dist/ -.vscode/ \ No newline at end of file +.vscode/ +docs/ diff --git a/package.json b/package.json index 20dfe62c..c6e3e82b 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "test:vitest": "vitest run", "test:vitest:unit": "vitest run --project unit", "test:vitest:browser": "vitest run --project browser", + "test:vitest:browser:headless": "vitest run --project browser-headless", "test:vitest:watch": "vitest", "test:package": "npm run test:eslint && npm run test:exports", "test:eslint": "node bin/ssl_hotfix.js eslint lib/**/*.js", diff --git a/test/specs/core/enhanceError.spec.js b/test/specs/core/enhanceError.spec.js deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/browser/adapter.browser.test.js b/tests/browser/adapter.browser.test.js new file mode 100644 index 00000000..d9df5466 --- /dev/null +++ b/tests/browser/adapter.browser.test.js @@ -0,0 +1,178 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import axios from '../../index.js'; + +class MockXMLHttpRequest { + constructor() { + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.response = null; + this.onreadystatechange = null; + this.onloadend = null; + this.upload = { + addEventListener() {}, + }; + } + + open(method, url, async = true) { + this.method = method; + this.url = url; + this.async = async; + } + + setRequestHeader() {} + + addEventListener() {} + + getAllResponseHeaders() { + return ''; + } + + send(data) { + this.params = data; + requests.push(this); + } + + respondWith({ status = 200, statusText = 'OK', responseText = '' } = {}) { + this.status = status; + this.statusText = statusText; + this.responseText = responseText; + this.response = responseText; + this.readyState = 4; + + queueMicrotask(() => { + if (this.onloadend) { + this.onloadend(); + } else if (this.onreadystatechange) { + this.onreadystatechange(); + } + }); + } + + abort() {} +} + +let requests = []; +let OriginalXMLHttpRequest; + +const sleep = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)); + +const waitForRequest = async (timeoutMs = 1000) => { + const start = Date.now(); + + while (Date.now() - start < timeoutMs) { + const request = requests.at(-1); + if (request) { + return request; + } + + await sleep(0); + } + + throw new Error('Expected an XHR request to be sent'); +}; + +describe('adapter (vitest browser)', () => { + beforeEach(() => { + requests = []; + OriginalXMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(() => { + window.XMLHttpRequest = OriginalXMLHttpRequest; + axios.interceptors.request.handlers = []; + axios.interceptors.response.handlers = []; + }); + + it('should support custom adapter', async () => { + const responsePromise = axios('/foo', { + adapter(config) { + return new Promise((resolve) => { + const request = new XMLHttpRequest(); + request.open('GET', '/bar'); + + request.onreadystatechange = function onReadyStateChange() { + resolve({ + config, + request, + }); + }; + + request.send(null); + }); + }, + }); + + const request = await waitForRequest(); + expect(request.url).toBe('/bar'); + + request.respondWith(); + await responsePromise; + }); + + it('should execute adapter code synchronously', async () => { + let asyncFlag = false; + + const responsePromise = axios('/foo', { + adapter(config) { + return new Promise((resolve) => { + const request = new XMLHttpRequest(); + request.open('GET', '/bar'); + + request.onreadystatechange = function onReadyStateChange() { + resolve({ + config, + request, + }); + }; + + expect(asyncFlag).toBe(false); + request.send(null); + }); + }, + }); + + asyncFlag = true; + + const request = await waitForRequest(); + request.respondWith(); + await responsePromise; + }); + + it('should execute adapter code asynchronously when interceptor is present', async () => { + let asyncFlag = false; + + axios.interceptors.request.use((config) => { + config.headers.async = 'async it!'; + return config; + }); + + const responsePromise = axios('/foo', { + adapter(config) { + return new Promise((resolve) => { + const request = new XMLHttpRequest(); + request.open('GET', '/bar'); + + request.onreadystatechange = function onReadyStateChange() { + resolve({ + config, + request, + }); + }; + + expect(asyncFlag).toBe(true); + request.send(null); + }); + }, + }); + + asyncFlag = true; + + const request = await waitForRequest(); + request.respondWith(); + await responsePromise; + }); +}); diff --git a/tests/browser/basicAuth.browser.test.js b/tests/browser/basicAuth.browser.test.js new file mode 100644 index 00000000..d6da5225 --- /dev/null +++ b/tests/browser/basicAuth.browser.test.js @@ -0,0 +1,136 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import axios from '../../index.js'; + +class MockXMLHttpRequest { + constructor() { + this.requestHeaders = {}; + this.responseHeaders = ''; + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.response = null; + this.onreadystatechange = null; + this.onloadend = null; + this.upload = { + addEventListener() {}, + }; + } + + open(method, url, async = true) { + this.method = method; + this.url = url; + this.async = async; + } + + setRequestHeader(key, value) { + this.requestHeaders[key] = value; + } + + addEventListener() {} + + getAllResponseHeaders() { + return this.responseHeaders; + } + + send(data) { + this.params = data; + requests.push(this); + } + + respondWith({ status = 200, statusText = 'OK', responseText = '', responseHeaders = '' } = {}) { + this.status = status; + this.statusText = statusText; + this.responseText = responseText; + this.response = responseText; + this.responseHeaders = responseHeaders; + this.readyState = 4; + + queueMicrotask(() => { + if (this.onloadend) { + this.onloadend(); + } else if (this.onreadystatechange) { + this.onreadystatechange(); + } + }); + } + + abort() {} +} + +let requests = []; +let OriginalXMLHttpRequest; + +const startRequest = (...args) => { + const promise = axios(...args); + const request = requests.at(-1); + + expect(request).toBeDefined(); + + return { request, promise }; +}; + +const flushSuccess = async (request, promise) => { + request.respondWith({ status: 200 }); + await promise; +}; + +describe('basicAuth (vitest browser)', () => { + beforeEach(() => { + requests = []; + OriginalXMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(() => { + window.XMLHttpRequest = OriginalXMLHttpRequest; + }); + + it('should accept HTTP Basic auth with username/password', async () => { + const { request, promise } = startRequest('/foo', { + auth: { + username: 'Aladdin', + password: 'open sesame', + }, + }); + + expect(request.requestHeaders.Authorization).toBe('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='); + + await flushSuccess(request, promise); + }); + + it('should accept HTTP Basic auth credentials without the password parameter', async () => { + const { request, promise } = startRequest('/foo', { + auth: { + username: 'Aladdin', + }, + }); + + expect(request.requestHeaders.Authorization).toBe('Basic QWxhZGRpbjo='); + + await flushSuccess(request, promise); + }); + + it('should accept HTTP Basic auth credentials with non-Latin1 characters in password', async () => { + const { request, promise } = startRequest('/foo', { + auth: { + username: 'Aladdin', + password: 'open ßç£☃sesame', + }, + }); + + expect(request.requestHeaders.Authorization).toBe('Basic QWxhZGRpbjpvcGVuIMOfw6fCo+KYg3Nlc2FtZQ=='); + + await flushSuccess(request, promise); + }); + + it('should fail to encode HTTP Basic auth credentials with non-Latin1 characters in username', async () => { + await expect(axios('/foo', { + auth: { + username: 'Aladßç£☃din', + password: 'open sesame', + }, + })).rejects.toThrow(/character/i); + }); +}); diff --git a/tests/browser/cancel.browser.test.js b/tests/browser/cancel.browser.test.js new file mode 100644 index 00000000..96cf658a --- /dev/null +++ b/tests/browser/cancel.browser.test.js @@ -0,0 +1,173 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import axios from '../../index.js'; + +class MockXMLHttpRequest { + constructor() { + this.requestHeaders = {}; + this.responseHeaders = ''; + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.response = null; + this.onreadystatechange = null; + this.onloadend = null; + this.onabort = null; + this.upload = { + addEventListener() {}, + }; + } + + open(method, url, async = true) { + this.method = method; + this.url = url; + this.async = async; + } + + setRequestHeader(key, value) { + this.requestHeaders[key] = value; + } + + addEventListener() {} + + getAllResponseHeaders() { + return this.responseHeaders; + } + + send(data) { + this.params = data; + requests.push(this); + } + + respondWith({ status = 200, statusText = 'OK', responseText = '', responseHeaders = '' } = {}) { + this.status = status; + this.statusText = statusText; + this.responseText = responseText; + this.response = responseText; + this.responseHeaders = responseHeaders; + this.readyState = 4; + + queueMicrotask(() => { + if (this.onloadend) { + this.onloadend(); + } else if (this.onreadystatechange) { + this.onreadystatechange(); + } + }); + } + + abort() { + this.statusText = 'abort'; + if (this.onabort) { + this.onabort(); + } + } +} + +let requests = []; +let OriginalXMLHttpRequest; + +const waitForRequest = async (timeoutMs = 1000) => { + const start = Date.now(); + + while (Date.now() - start < timeoutMs) { + const request = requests.at(-1); + if (request) { + return request; + } + + await Promise.resolve(); + } + + throw new Error('Expected an XHR request to be sent'); +}; + +describe('cancel (vitest browser)', () => { + beforeEach(() => { + requests = []; + OriginalXMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(() => { + window.XMLHttpRequest = OriginalXMLHttpRequest; + }); + + describe('when called before sending request', () => { + it('rejects Promise with a CanceledError object', async () => { + const source = axios.CancelToken.source(); + source.cancel('Operation has been canceled.'); + + const error = await axios + .get('/foo', { + cancelToken: source.token, + }) + .catch((thrown) => thrown); + + expect(axios.isCancel(error)).toBe(true); + expect(error.message).toBe('Operation has been canceled.'); + expect(requests).toHaveLength(0); + }); + }); + + describe('when called after request has been sent', () => { + it('rejects Promise with a CanceledError object', async () => { + const source = axios.CancelToken.source(); + const promise = axios.get('/foo/bar', { + cancelToken: source.token, + }); + + const request = await waitForRequest(); + + // Call cancel() after the request has been sent, but before response is received. + source.cancel('Operation has been canceled.'); + request.respondWith({ + status: 200, + responseText: 'OK', + }); + + const error = await promise.catch((thrown) => thrown); + + expect(axios.isCancel(error)).toBe(true); + expect(error.message).toBe('Operation has been canceled.'); + }); + + it('calls abort on request object', async () => { + const source = axios.CancelToken.source(); + const promise = axios.get('/foo/bar', { + cancelToken: source.token, + }); + + const request = await waitForRequest(); + + // Call cancel() after the request has been sent, but before response is received. + source.cancel(); + + await promise.catch(() => undefined); + + expect(request.statusText).toBe('abort'); + }); + }); + + it('supports cancellation using AbortController signal', async () => { + const controller = new AbortController(); + const promise = axios.get('/foo/bar', { + signal: controller.signal, + }); + + const request = await waitForRequest(); + + // Call abort() after the request has been sent, but before response is received. + controller.abort(); + setTimeout(() => { + request.respondWith({ + status: 200, + responseText: 'OK', + }); + }, 0); + + const error = await promise.catch((thrown) => thrown); + expect(axios.isCancel(error)).toBe(true); + }); +}); diff --git a/tests/browser/cancelToken.browser.test.js b/tests/browser/cancelToken.browser.test.js new file mode 100644 index 00000000..4c1b4c2f --- /dev/null +++ b/tests/browser/cancelToken.browser.test.js @@ -0,0 +1,90 @@ +import { describe, expect, it } from 'vitest'; + +import CancelToken from '../../lib/cancel/CancelToken.js'; +import CanceledError from '../../lib/cancel/CanceledError.js'; + +describe('CancelToken (vitest browser)', () => { + describe('constructor', () => { + it('throws when executor is not specified', () => { + expect(() => new CancelToken()).toThrowError( + new TypeError('executor must be a function.') + ); + }); + + it('throws when executor is not a function', () => { + expect(() => new CancelToken(123)).toThrowError( + new TypeError('executor must be a function.') + ); + }); + }); + + describe('reason', () => { + it('returns a CanceledError if cancellation has been requested', () => { + let cancel; + const token = new CancelToken((c) => { + cancel = c; + }); + + cancel('Operation has been canceled.'); + + expect(token.reason).toBeInstanceOf(CanceledError); + expect(token.reason?.message).toBe('Operation has been canceled.'); + }); + + it('returns undefined if cancellation has not been requested', () => { + const token = new CancelToken(() => {}); + + expect(token.reason).toBeUndefined(); + }); + }); + + describe('promise', () => { + it('resolves when cancellation is requested', async () => { + let cancel; + const token = new CancelToken((c) => { + cancel = c; + }); + + cancel('Operation has been canceled.'); + const reason = await token.promise; + + expect(reason).toBeInstanceOf(CanceledError); + expect(reason.message).toBe('Operation has been canceled.'); + }); + }); + + describe('throwIfRequested', () => { + it('throws if cancellation has been requested', () => { + let cancel; + const token = new CancelToken((c) => { + cancel = c; + }); + + cancel('Operation has been canceled.'); + + expect(() => token.throwIfRequested()).toThrow(CanceledError); + expect(() => token.throwIfRequested()).toThrow('Operation has been canceled.'); + }); + + it('does not throw if cancellation has not been requested', () => { + const token = new CancelToken(() => {}); + + expect(() => token.throwIfRequested()).not.toThrow(); + }); + }); + + describe('source', () => { + it('returns an object containing token and cancel function', () => { + const source = CancelToken.source(); + + expect(source.token).toBeInstanceOf(CancelToken); + expect(source.cancel).toBeTypeOf('function'); + expect(source.token.reason).toBeUndefined(); + + source.cancel('Operation has been canceled.'); + + expect(source.token.reason).toBeInstanceOf(CanceledError); + expect(source.token.reason?.message).toBe('Operation has been canceled.'); + }); + }); +}); diff --git a/tests/browser/cookies.browser.test.js b/tests/browser/cookies.browser.test.js new file mode 100644 index 00000000..6a2a3e1c --- /dev/null +++ b/tests/browser/cookies.browser.test.js @@ -0,0 +1,56 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import cookies from '../../lib/helpers/cookies.js'; + +const clearAllCookies = () => { + const expiry = new Date(Date.now() - 86400000).toUTCString(); + + for (const cookie of document.cookie.split(';')) { + const name = cookie.split('=')[0].trim(); + + if (!name) { + continue; + } + + // Clear both default-path and root-path cookies for the same key. + document.cookie = `${name}=; expires=${expiry}`; + document.cookie = `${name}=; expires=${expiry}; path=/`; + } +}; + +describe('helpers::cookies (vitest browser)', () => { + beforeEach(() => { + clearAllCookies(); + }); + + afterEach(() => { + clearAllCookies(); + }); + + it('writes cookies', () => { + cookies.write('foo', 'baz'); + + expect(document.cookie).toBe('foo=baz'); + }); + + it('reads cookies', () => { + cookies.write('foo', 'abc'); + cookies.write('bar', 'def'); + + expect(cookies.read('foo')).toBe('abc'); + expect(cookies.read('bar')).toBe('def'); + }); + + it('removes cookies', () => { + cookies.write('foo', 'bar'); + cookies.remove('foo'); + + expect(cookies.read('foo')).toBeNull(); + }); + + it('uri encodes values', () => { + cookies.write('foo', 'bar baz%'); + + expect(document.cookie).toBe('foo=bar%20baz%25'); + }); +}); diff --git a/tests/browser/defaults.browser.test.js b/tests/browser/defaults.browser.test.js new file mode 100644 index 00000000..f594f648 --- /dev/null +++ b/tests/browser/defaults.browser.test.js @@ -0,0 +1,282 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import axios from '../../index.js'; +import defaults from '../../lib/defaults/index.js'; +import AxiosHeaders from '../../lib/core/AxiosHeaders.js'; + +class MockXMLHttpRequest { + constructor() { + this.requestHeaders = {}; + this.responseHeaders = ''; + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.response = null; + this.onreadystatechange = null; + this.onloadend = null; + this.upload = { + addEventListener() {}, + }; + } + + open(method, url, async = true) { + this.method = method; + this.url = url; + this.async = async; + } + + setRequestHeader(key, value) { + this.requestHeaders[key] = value; + } + + addEventListener() {} + + getAllResponseHeaders() { + return this.responseHeaders; + } + + send(data) { + this.params = data; + requests.push(this); + } + + respondWith({ status = 200, statusText = 'OK', responseText = '', responseHeaders = '' } = {}) { + this.status = status; + this.statusText = statusText; + this.responseText = responseText; + this.response = responseText; + this.responseHeaders = responseHeaders; + this.readyState = 4; + + queueMicrotask(() => { + if (this.onloadend) { + this.onloadend(); + } else if (this.onreadystatechange) { + this.onreadystatechange(); + } + }); + } + + abort() {} +} + +const XSRF_COOKIE_NAME = 'CUSTOM-XSRF-TOKEN'; + +let requests = []; +let OriginalXMLHttpRequest; + +const getLastRequest = () => { + const request = requests.at(-1); + + expect(request).toBeDefined(); + + return request; +}; + +const finishRequest = async (request, promise) => { + request.respondWith({ status: 200 }); + await promise; +}; + +describe('defaults (vitest browser)', () => { + beforeEach(() => { + requests = []; + OriginalXMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(() => { + window.XMLHttpRequest = OriginalXMLHttpRequest; + delete axios.defaults.baseURL; + delete axios.defaults.headers.get['X-CUSTOM-HEADER']; + delete axios.defaults.headers.post['X-CUSTOM-HEADER']; + document.cookie = `${XSRF_COOKIE_NAME}=;expires=${new Date(Date.now() - 86400000).toUTCString()}`; + }); + + it('should transform request json', () => { + expect(defaults.transformRequest[0]({ foo: 'bar' }, new AxiosHeaders())).toBe('{"foo":"bar"}'); + }); + + it("should also transform request json when 'Content-Type' is 'application/json'", () => { + const headers = new AxiosHeaders({ + 'Content-Type': 'application/json', + }); + + expect(defaults.transformRequest[0](JSON.stringify({ foo: 'bar' }), headers)).toBe('{"foo":"bar"}'); + expect(defaults.transformRequest[0]([42, 43], headers)).toBe('[42,43]'); + expect(defaults.transformRequest[0]('foo', headers)).toBe('"foo"'); + expect(defaults.transformRequest[0](42, headers)).toBe('42'); + expect(defaults.transformRequest[0](true, headers)).toBe('true'); + expect(defaults.transformRequest[0](false, headers)).toBe('false'); + expect(defaults.transformRequest[0](null, headers)).toBe('null'); + }); + + it("should transform the plain data object to a FormData instance when header is 'multipart/form-data'", () => { + const headers = new AxiosHeaders({ + 'Content-Type': 'multipart/form-data', + }); + + const transformed = defaults.transformRequest[0]({ x: 1 }, headers); + + expect(transformed).toBeInstanceOf(FormData); + }); + + it('should do nothing to request string', () => { + expect(defaults.transformRequest[0]('foo=bar', new AxiosHeaders())).toBe('foo=bar'); + }); + + it('should transform response json', () => { + const data = defaults.transformResponse[0].call(defaults, '{"foo":"bar"}'); + + expect(typeof data).toBe('object'); + expect(data.foo).toBe('bar'); + }); + + it('should do nothing to response string', () => { + expect(defaults.transformResponse[0]('foo=bar')).toBe('foo=bar'); + }); + + it('should use global defaults config', async () => { + const promise = axios('/foo'); + const request = getLastRequest(); + + expect(request.url).toBe('/foo'); + + await finishRequest(request, promise); + }); + + it('should use modified defaults config', async () => { + axios.defaults.baseURL = 'http://example.com/'; + + const promise = axios('/foo'); + const request = getLastRequest(); + + expect(request.url).toBe('http://example.com/foo'); + + await finishRequest(request, promise); + }); + + it('should use request config', async () => { + const promise = axios('/foo', { + baseURL: 'http://www.example.com', + }); + const request = getLastRequest(); + + expect(request.url).toBe('http://www.example.com/foo'); + + await finishRequest(request, promise); + }); + + it('should use default config for custom instance', async () => { + const instance = axios.create({ + xsrfCookieName: XSRF_COOKIE_NAME, + xsrfHeaderName: 'X-CUSTOM-XSRF-TOKEN', + }); + document.cookie = `${instance.defaults.xsrfCookieName}=foobarbaz`; + + const promise = instance.get('/foo'); + const request = getLastRequest(); + + expect(request.requestHeaders[instance.defaults.xsrfHeaderName]).toBe('foobarbaz'); + + await finishRequest(request, promise); + }); + + it('should use GET headers', async () => { + axios.defaults.headers.get['X-CUSTOM-HEADER'] = 'foo'; + + const promise = axios.get('/foo'); + const request = getLastRequest(); + + expect(request.requestHeaders['X-CUSTOM-HEADER']).toBe('foo'); + + await finishRequest(request, promise); + }); + + it('should use POST headers', async () => { + axios.defaults.headers.post['X-CUSTOM-HEADER'] = 'foo'; + + const promise = axios.post('/foo', {}); + const request = getLastRequest(); + + expect(request.requestHeaders['X-CUSTOM-HEADER']).toBe('foo'); + + await finishRequest(request, promise); + }); + + it('should use header config', async () => { + const instance = axios.create({ + headers: { + common: { + 'X-COMMON-HEADER': 'commonHeaderValue', + }, + get: { + 'X-GET-HEADER': 'getHeaderValue', + }, + post: { + 'X-POST-HEADER': 'postHeaderValue', + }, + }, + }); + + const promise = instance.get('/foo', { + headers: { + 'X-FOO-HEADER': 'fooHeaderValue', + 'X-BAR-HEADER': 'barHeaderValue', + }, + }); + const request = getLastRequest(); + + expect(request.requestHeaders).toEqual( + AxiosHeaders.concat(defaults.headers.common, defaults.headers.get, { + 'X-COMMON-HEADER': 'commonHeaderValue', + 'X-GET-HEADER': 'getHeaderValue', + 'X-FOO-HEADER': 'fooHeaderValue', + 'X-BAR-HEADER': 'barHeaderValue', + }).toJSON() + ); + + await finishRequest(request, promise); + }); + + it('should be used by custom instance if set before instance created', async () => { + axios.defaults.baseURL = 'http://example.org/'; + const instance = axios.create(); + + const promise = instance.get('/foo'); + const request = getLastRequest(); + + expect(request.url).toBe('http://example.org/foo'); + + await finishRequest(request, promise); + }); + + it('should not be used by custom instance if set after instance created', async () => { + const instance = axios.create(); + axios.defaults.baseURL = 'http://example.org/'; + + const promise = instance.get('/foo/users'); + const request = getLastRequest(); + + expect(request.url).toBe('/foo/users'); + + await finishRequest(request, promise); + }); + + it('should resistant to ReDoS attack', async () => { + const instance = axios.create(); + const start = performance.now(); + const slashes = '/'.repeat(100000); + instance.defaults.baseURL = `/${slashes}bar/`; + + const promise = instance.get('/foo'); + const request = getLastRequest(); + const elapsedTimeMs = performance.now() - start; + + expect(elapsedTimeMs).toBeLessThan(20); + expect(request.url).toBe(`/${slashes}bar/foo`); + + await finishRequest(request, promise); + }); +}); diff --git a/tests/browser/formdata.browser.test.js b/tests/browser/formdata.browser.test.js new file mode 100644 index 00000000..50612134 --- /dev/null +++ b/tests/browser/formdata.browser.test.js @@ -0,0 +1,104 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import axios from '../../index.js'; + +class MockXMLHttpRequest { + constructor() { + this.requestHeaders = {}; + this.responseHeaders = ''; + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.response = null; + this.onreadystatechange = null; + this.onloadend = null; + this.upload = { + addEventListener() {}, + }; + } + + open(method, url, async = true) { + this.method = method; + this.url = url; + this.async = async; + } + + setRequestHeader(key, value) { + this.requestHeaders[key] = value; + } + + addEventListener() {} + + getAllResponseHeaders() { + return this.responseHeaders; + } + + send(data) { + this.params = data; + requests.push(this); + } + + respondWith({ status = 200, statusText = 'OK', responseText = '', responseHeaders = '' } = {}) { + this.status = status; + this.statusText = statusText; + this.responseText = responseText; + this.response = responseText; + this.responseHeaders = responseHeaders; + this.readyState = 4; + + queueMicrotask(() => { + if (this.onloadend) { + this.onloadend(); + } else if (this.onreadystatechange) { + this.onreadystatechange(); + } + }); + } + + abort() {} +} + +let requests = []; +let OriginalXMLHttpRequest; + +const getLastRequest = () => { + const request = requests.at(-1); + + expect(request).toBeDefined(); + + return request; +}; + +describe('formdata (vitest browser)', () => { + beforeEach(() => { + requests = []; + OriginalXMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(() => { + window.XMLHttpRequest = OriginalXMLHttpRequest; + }); + + it('should allow FormData posting', async () => { + const responsePromise = axios.postForm('/foo', { + a: 'foo', + b: 'bar', + }); + const request = getLastRequest(); + + expect(request.params).toBeInstanceOf(FormData); + expect(Object.fromEntries(request.params.entries())).toEqual({ + a: 'foo', + b: 'bar', + }); + + request.respondWith({ + status: 200, + responseText: '{}', + responseHeaders: 'Content-Type: application/json', + }); + await responsePromise; + }); +}); diff --git a/tests/browser/headers.browser.test.js b/tests/browser/headers.browser.test.js new file mode 100644 index 00000000..306c466c --- /dev/null +++ b/tests/browser/headers.browser.test.js @@ -0,0 +1,228 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import axios, { AxiosHeaders } from '../../index.js'; + +class MockXMLHttpRequest { + constructor() { + this.requestHeaders = {}; + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.response = null; + this.onreadystatechange = null; + this.onloadend = null; + this.upload = { + addEventListener() {}, + }; + } + + open(method, url, async = true) { + this.method = method; + this.url = url; + this.async = async; + } + + setRequestHeader(key, value) { + this.requestHeaders[key] = value; + } + + addEventListener() {} + + getAllResponseHeaders() { + return ''; + } + + send(data) { + this.params = data; + requests.push(this); + } + + respondWith({ status = 200, statusText = 'OK', responseText = '' } = {}) { + this.status = status; + this.statusText = statusText; + this.responseText = responseText; + this.response = responseText; + this.readyState = 4; + + queueMicrotask(() => { + if (this.onloadend) { + this.onloadend(); + } else if (this.onreadystatechange) { + this.onreadystatechange(); + } + }); + } + + abort() {} +} + +let requests = []; +let OriginalXMLHttpRequest; + +const getLastRequest = () => { + const request = requests.at(-1); + + expect(request).toBeDefined(); + + return request; +}; + +const finishRequest = async (request, promise) => { + request.respondWith({ status: 200 }); + await promise; +}; + +function testHeaderValue(headers, key, val) { + let found = false; + + for (const k in headers) { + if (k.toLowerCase() === key.toLowerCase()) { + found = true; + expect(headers[k]).toBe(val); + break; + } + } + + if (!found) { + if (typeof val === 'undefined') { + expect(Object.prototype.hasOwnProperty.call(headers, key)).toBe(false); + } else { + throw new Error(`${key} was not found in headers`); + } + } +} + +describe('headers (vitest browser)', () => { + beforeEach(() => { + requests = []; + OriginalXMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(() => { + window.XMLHttpRequest = OriginalXMLHttpRequest; + }); + + it('should default common headers', async () => { + const headers = axios.defaults.headers.common; + const promise = axios('/foo'); + const request = getLastRequest(); + + for (const key in headers) { + if (Object.prototype.hasOwnProperty.call(headers, key)) { + expect(request.requestHeaders[key]).toBe(headers[key]); + } + } + + await finishRequest(request, promise); + }); + + it('should respect common Content-Type header', async () => { + const instance = axios.create(); + instance.defaults.headers.common['Content-Type'] = 'application/custom'; + + const promise = instance.patch('/foo', ''); + const request = getLastRequest(); + + expect(request.requestHeaders['Content-Type']).toBe('application/custom'); + + await finishRequest(request, promise); + }); + + it('should add extra headers for post', async () => { + const headers = AxiosHeaders.from(axios.defaults.headers.common).toJSON(); + const promise = axios.post('/foo', 'fizz=buzz'); + const request = getLastRequest(); + + for (const key in headers) { + expect(request.requestHeaders[key]).toBe(headers[key]); + } + + await finishRequest(request, promise); + }); + + it('should reset headers by null or explicit undefined', async () => { + const promise = axios.create({ + headers: { + common: { + 'x-header-a': 'a', + 'x-header-b': 'b', + 'x-header-c': 'c', + }, + }, + }).post( + '/foo', + { fizz: 'buzz' }, + { + headers: { + 'Content-Type': null, + 'x-header-a': null, + 'x-header-b': undefined, + }, + } + ); + const request = getLastRequest(); + + testHeaderValue(request.requestHeaders, 'Content-Type', undefined); + testHeaderValue(request.requestHeaders, 'x-header-a', undefined); + testHeaderValue(request.requestHeaders, 'x-header-b', undefined); + testHeaderValue(request.requestHeaders, 'x-header-c', 'c'); + + await finishRequest(request, promise); + }); + + it('should use application/json when posting an object', async () => { + const promise = axios.post('/foo/bar', { + firstName: 'foo', + lastName: 'bar', + }); + const request = getLastRequest(); + + testHeaderValue(request.requestHeaders, 'Content-Type', 'application/json'); + + await finishRequest(request, promise); + }); + + it('should remove content-type if data is empty', async () => { + const promise = axios.post('/foo'); + const request = getLastRequest(); + + testHeaderValue(request.requestHeaders, 'Content-Type', undefined); + + await finishRequest(request, promise); + }); + + it('should preserve content-type if data is false', async () => { + const promise = axios.post('/foo', false); + const request = getLastRequest(); + + testHeaderValue(request.requestHeaders, 'Content-Type', 'application/x-www-form-urlencoded'); + + await finishRequest(request, promise); + }); + + it('should allow an AxiosHeaders instance to be used as the value of the headers option', async () => { + const instance = axios.create({ + headers: new AxiosHeaders({ + xFoo: 'foo', + xBar: 'bar', + }), + }); + + const promise = instance.get('/foo', { + headers: { + XFOO: 'foo2', + xBaz: 'baz', + }, + }); + const request = getLastRequest(); + + expect(request.requestHeaders.xFoo).toBe('foo2'); + expect(request.requestHeaders.xBar).toBe('bar'); + expect(request.requestHeaders.xBaz).toBe('baz'); + expect(request.requestHeaders.XFOO).toBeUndefined(); + + await finishRequest(request, promise); + }); +}); diff --git a/tests/browser/instance.browser.test.js b/tests/browser/instance.browser.test.js new file mode 100644 index 00000000..df33294e --- /dev/null +++ b/tests/browser/instance.browser.test.js @@ -0,0 +1,255 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import axios from '../../index.js'; + +class MockXMLHttpRequest { + constructor() { + this.requestHeaders = {}; + this.responseHeaders = ''; + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.response = null; + this.onreadystatechange = null; + this.onloadend = null; + this.upload = { + addEventListener() {}, + }; + } + + open(method, url, async = true) { + this.method = method; + this.url = url; + this.async = async; + } + + setRequestHeader(key, value) { + this.requestHeaders[key] = value; + } + + addEventListener() {} + + getAllResponseHeaders() { + return this.responseHeaders; + } + + send(data) { + this.params = data; + requests.push(this); + } + + respondWith({ status = 200, statusText = 'OK', responseText = '', responseHeaders = '' } = {}) { + this.status = status; + this.statusText = statusText; + this.responseText = responseText; + this.response = responseText; + this.responseHeaders = responseHeaders; + this.readyState = 4; + + queueMicrotask(() => { + if (this.onloadend) { + this.onloadend(); + } else if (this.onreadystatechange) { + this.onreadystatechange(); + } + }); + } +} + +let requests = []; +let OriginalXMLHttpRequest; + +const getLastRequest = () => { + const request = requests.at(-1); + + expect(request).toBeDefined(); + + return request; +}; + +const flushSuccess = async (request, promise) => { + request.respondWith({ status: 200 }); + await promise; +}; + +const waitForRequest = async (timeoutMs = 1000) => { + const start = Date.now(); + + while (Date.now() - start < timeoutMs) { + const request = requests.at(-1); + if (request) { + return request; + } + + await Promise.resolve(); + } + + throw new Error('Expected an XHR request to be sent'); +}; + +describe('instance (vitest browser)', () => { + beforeEach(() => { + requests = []; + OriginalXMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(() => { + window.XMLHttpRequest = OriginalXMLHttpRequest; + }); + + it('should have the same methods as default instance', () => { + const instance = axios.create(); + + for (const prop in axios) { + if ( + [ + 'Axios', + 'AxiosError', + 'create', + 'Cancel', + 'CanceledError', + 'CancelToken', + 'isCancel', + 'all', + 'spread', + 'getUri', + 'isAxiosError', + 'mergeConfig', + 'getAdapter', + 'VERSION', + 'default', + 'toFormData', + 'formToJSON', + 'AxiosHeaders', + 'HttpStatusCode', + ].includes(prop) + ) { + continue; + } + + expect(typeof instance[prop]).toBe(typeof axios[prop]); + } + }); + + it('should make an http request without verb helper', async () => { + const instance = axios.create(); + const promise = instance('/foo'); + const request = getLastRequest(); + + expect(request.url).toBe('/foo'); + + await flushSuccess(request, promise); + }); + + it('should make an http request with url instead of baseURL', async () => { + const instance = axios.create({ + url: 'https://api.example.com', + }); + const promise = instance('/foo'); + const request = getLastRequest(); + + expect(request.url).toBe('/foo'); + + await flushSuccess(request, promise); + }); + + it('should make an http request', async () => { + const instance = axios.create(); + const promise = instance.get('/foo'); + const request = getLastRequest(); + + expect(request.url).toBe('/foo'); + + await flushSuccess(request, promise); + }); + + it('should use instance options', async () => { + const instance = axios.create({ timeout: 1000 }); + const promise = instance.get('/foo'); + const request = getLastRequest(); + + expect(request.timeout).toBe(1000); + + await flushSuccess(request, promise); + }); + + it('should have defaults.headers', () => { + const instance = axios.create({ + baseURL: 'https://api.example.com', + }); + + expect(typeof instance.defaults.headers).toBe('object'); + expect(typeof instance.defaults.headers.common).toBe('object'); + }); + + it('should have interceptors on the instance', async () => { + const requestInterceptorId = axios.interceptors.request.use((config) => { + config.foo = true; + return config; + }); + + const instance = axios.create(); + const instanceInterceptorId = instance.interceptors.request.use((config) => { + config.bar = true; + return config; + }); + + try { + const responsePromise = instance.get('/foo'); + const request = await waitForRequest(); + + request.respondWith({ + status: 200, + }); + + const response = await responsePromise; + + expect(response.config.foo).toBeUndefined(); + expect(response.config.bar).toBe(true); + } finally { + axios.interceptors.request.eject(requestInterceptorId); + instance.interceptors.request.eject(instanceInterceptorId); + } + }); + + it('should have getUri on the instance', () => { + const instance = axios.create({ + baseURL: 'https://api.example.com', + }); + const options = { + url: 'foo/bar', + params: { + name: 'axios', + }, + }; + + expect(instance.getUri(options)).toBe('https://api.example.com/foo/bar?name=axios'); + }); + + it('should correctly build url without baseURL', () => { + const instance = axios.create(); + const options = { + url: 'foo/bar?foo=bar', + params: { + name: 'axios', + }, + }; + + expect(instance.getUri(options)).toBe('foo/bar?foo=bar&name=axios'); + }); + + it('should correctly discard url hash mark', () => { + const instance = axios.create(); + const options = { + baseURL: 'https://api.example.com', + url: 'foo/bar?foo=bar#hash', + params: { + name: 'axios', + }, + }; + + expect(instance.getUri(options)).toBe('https://api.example.com/foo/bar?foo=bar&name=axios'); + }); +}); diff --git a/tests/browser/interceptors.browser.test.js b/tests/browser/interceptors.browser.test.js new file mode 100644 index 00000000..144c8064 --- /dev/null +++ b/tests/browser/interceptors.browser.test.js @@ -0,0 +1,745 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import axios from '../../index.js'; + +class MockXMLHttpRequest { + constructor() { + this.requestHeaders = {}; + this.responseHeaders = {}; + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.response = null; + this.responseURL = ''; + this.timeout = 0; + this.withCredentials = false; + this.onreadystatechange = null; + this.onloadend = null; + this.onabort = null; + this.onerror = null; + this.ontimeout = null; + this.upload = { + addEventListener() {}, + }; + } + + open(method, url, async = true) { + this.method = method; + this.url = url; + this.async = async; + } + + setRequestHeader(key, value) { + this.requestHeaders[key] = value; + } + + addEventListener() {} + + getAllResponseHeaders() { + if (typeof this.responseHeaders === 'string') { + return this.responseHeaders; + } + + return Object.entries(this.responseHeaders) + .map(([key, value]) => `${key}: ${value}`) + .join('\n'); + } + + send(data) { + this.params = data; + requests.push(this); + this.readyState = 1; + } + + respondWith({ + status = 200, + statusText = 'OK', + responseText = '', + response = null, + responseHeaders = {}, + headers = {}, + responseURL = '', + } = {}) { + this.status = status; + this.statusText = statusText; + this.responseText = responseText; + this.response = response === null ? responseText : response; + this.responseHeaders = Object.keys(headers).length ? headers : responseHeaders; + this.responseURL = responseURL; + this.readyState = 4; + this.finish(); + } + + responseTimeout() { + if (this.ontimeout) { + this.ontimeout(); + } + } + + failNetworkError(message = 'Network Error') { + if (this.onerror) { + this.onerror({ message }); + } + } + + abort() { + if (this.onabort) { + this.onabort(); + } + } + + finish() { + queueMicrotask(() => { + if (this.onloadend) { + this.onloadend(); + } else if (this.onreadystatechange) { + this.onreadystatechange(); + } + }); + } +} + +let requests = []; +let OriginalXMLHttpRequest; + +const sleep = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)); + +const waitForRequest = async (timeoutMs = 1000) => { + const start = Date.now(); + + while (Date.now() - start < timeoutMs) { + const request = requests.at(-1); + if (request) { + return request; + } + + await sleep(0); + } + + throw new Error('Expected an XHR request to be sent'); +}; + +describe('interceptors (vitest browser)', () => { + beforeEach(() => { + requests = []; + OriginalXMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(() => { + window.XMLHttpRequest = OriginalXMLHttpRequest; + axios.interceptors.request.handlers = []; + axios.interceptors.response.handlers = []; + vi.restoreAllMocks(); + }); + + it('should add a request interceptor (asynchronous by default)', async () => { + let asyncFlag = false; + + axios.interceptors.request.use((config) => { + config.headers.test = 'added by interceptor'; + expect(asyncFlag).toBe(true); + return config; + }); + + const responsePromise = axios('/foo'); + asyncFlag = true; + + const request = await waitForRequest(); + expect(request.requestHeaders.test).toBe('added by interceptor'); + request.respondWith(); + await responsePromise; + }); + + it('should add a request interceptor (explicitly flagged as asynchronous)', async () => { + let asyncFlag = false; + + axios.interceptors.request.use( + (config) => { + config.headers.test = 'added by interceptor'; + expect(asyncFlag).toBe(true); + return config; + }, + null, + { synchronous: false } + ); + + const responsePromise = axios('/foo'); + asyncFlag = true; + + const request = await waitForRequest(); + expect(request.requestHeaders.test).toBe('added by interceptor'); + request.respondWith(); + await responsePromise; + }); + + it('should add a request interceptor that is executed synchronously when flag is provided', async () => { + let asyncFlag = false; + + axios.interceptors.request.use( + (config) => { + config.headers.test = 'added by synchronous interceptor'; + expect(asyncFlag).toBe(false); + return config; + }, + null, + { synchronous: true } + ); + + const responsePromise = axios('/foo'); + asyncFlag = true; + + const request = await waitForRequest(); + expect(request.requestHeaders.test).toBe('added by synchronous interceptor'); + request.respondWith(); + await responsePromise; + }); + + it('should execute asynchronously when not all interceptors are explicitly flagged as synchronous', async () => { + let asyncFlag = false; + + axios.interceptors.request.use((config) => { + config.headers.foo = 'uh oh, async'; + expect(asyncFlag).toBe(true); + return config; + }); + + axios.interceptors.request.use( + (config) => { + config.headers.test = 'added by synchronous interceptor'; + expect(asyncFlag).toBe(true); + return config; + }, + null, + { synchronous: true } + ); + + axios.interceptors.request.use((config) => { + config.headers.test = 'added by the async interceptor'; + expect(asyncFlag).toBe(true); + return config; + }); + + const responsePromise = axios('/foo'); + asyncFlag = true; + + const request = await waitForRequest(); + expect(request.requestHeaders.foo).toBe('uh oh, async'); + expect(request.requestHeaders.test).toBe('added by synchronous interceptor'); + request.respondWith(); + await responsePromise; + }); + + it('should execute request interceptor in legacy order', async () => { + let sequence = ''; + + axios.interceptors.request.use((config) => { + sequence += '1'; + return config; + }); + + axios.interceptors.request.use((config) => { + sequence += '2'; + return config; + }); + + axios.interceptors.request.use((config) => { + sequence += '3'; + return config; + }); + + const responsePromise = axios({ url: '/foo' }); + const request = await waitForRequest(); + + expect(sequence).toBe('321'); + request.respondWith(); + await responsePromise; + }); + + it('should execute request interceptor in order', async () => { + let sequence = ''; + + axios.interceptors.request.use((config) => { + sequence += '1'; + return config; + }); + + axios.interceptors.request.use((config) => { + sequence += '2'; + return config; + }); + + axios.interceptors.request.use((config) => { + sequence += '3'; + return config; + }); + + const responsePromise = axios({ + url: '/foo', + transitional: { + legacyInterceptorReqResOrdering: false, + }, + }); + const request = await waitForRequest(); + + expect(sequence).toBe('123'); + request.respondWith(); + await responsePromise; + }); + + it('runs the interceptor if runWhen function is provided and resolves to true', async () => { + const onGetCall = (config) => config.method === 'get'; + + axios.interceptors.request.use( + (config) => { + config.headers.test = 'special get headers'; + return config; + }, + null, + { runWhen: onGetCall } + ); + + const responsePromise = axios('/foo'); + const request = await waitForRequest(); + + expect(request.requestHeaders.test).toBe('special get headers'); + request.respondWith(); + await responsePromise; + }); + + it('does not run the interceptor if runWhen function is provided and resolves to false', async () => { + const onPostCall = (config) => config.method === 'post'; + + axios.interceptors.request.use( + (config) => { + config.headers.test = 'special get headers'; + return config; + }, + null, + { runWhen: onPostCall } + ); + + const responsePromise = axios('/foo'); + const request = await waitForRequest(); + + expect(request.requestHeaders.test).toBeUndefined(); + request.respondWith(); + await responsePromise; + }); + + it('does not run async interceptor if runWhen resolves to false (and runs synchronously)', async () => { + let asyncFlag = false; + const onPostCall = (config) => config.method === 'post'; + + axios.interceptors.request.use( + (config) => { + config.headers.test = 'special get headers'; + return config; + }, + null, + { synchronous: false, runWhen: onPostCall } + ); + + axios.interceptors.request.use( + (config) => { + config.headers.sync = 'hello world'; + expect(asyncFlag).toBe(false); + return config; + }, + null, + { synchronous: true } + ); + + const responsePromise = axios('/foo'); + asyncFlag = true; + + const request = await waitForRequest(); + expect(request.requestHeaders.test).toBeUndefined(); + expect(request.requestHeaders.sync).toBe('hello world'); + request.respondWith(); + await responsePromise; + }); + + it('should call request onRejected when interceptor throws', async () => { + const rejectedSpy = vi.fn(); + const error = new Error('deadly error'); + + axios.interceptors.request.use( + () => { + throw error; + }, + rejectedSpy, + { synchronous: true } + ); + + const responsePromise = axios('/foo').catch(() => {}); + const request = await waitForRequest(); + request.respondWith(); + await responsePromise; + + expect(rejectedSpy).toHaveBeenCalledWith(error); + }); + + it('should add a request interceptor that returns a new config object', async () => { + axios.interceptors.request.use(() => ({ + url: '/bar', + method: 'post', + })); + + const responsePromise = axios('/foo'); + const request = await waitForRequest(); + + expect(request.method).toBe('POST'); + expect(request.url).toBe('/bar'); + request.respondWith(); + await responsePromise; + }); + + it('should add a request interceptor that returns a promise', async () => { + axios.interceptors.request.use((config) => + new Promise((resolve) => { + setTimeout(() => { + config.headers.async = 'promise'; + resolve(config); + }, 100); + }) + ); + + const responsePromise = axios('/foo'); + const request = await waitForRequest(1500); + + expect(request.requestHeaders.async).toBe('promise'); + request.respondWith(); + await responsePromise; + }); + + it('should add multiple request interceptors', async () => { + axios.interceptors.request.use((config) => { + config.headers.test1 = '1'; + return config; + }); + axios.interceptors.request.use((config) => { + config.headers.test2 = '2'; + return config; + }); + axios.interceptors.request.use((config) => { + config.headers.test3 = '3'; + return config; + }); + + const responsePromise = axios('/foo'); + const request = await waitForRequest(); + + expect(request.requestHeaders.test1).toBe('1'); + expect(request.requestHeaders.test2).toBe('2'); + expect(request.requestHeaders.test3).toBe('3'); + request.respondWith(); + await responsePromise; + }); + + it('should add a response interceptor', async () => { + axios.interceptors.response.use((data) => { + data.data = `${data.data} - modified by interceptor`; + return data; + }); + + const responsePromise = axios('/foo'); + const request = await waitForRequest(); + + request.respondWith({ + status: 200, + responseText: 'OK', + }); + + const response = await responsePromise; + expect(response.data).toBe('OK - modified by interceptor'); + }); + + it('should add a response interceptor when request interceptor is defined', async () => { + axios.interceptors.request.use((data) => data); + + axios.interceptors.response.use((data) => { + data.data = `${data.data} - modified by interceptor`; + return data; + }); + + const responsePromise = axios('/foo'); + const request = await waitForRequest(); + + request.respondWith({ + status: 200, + responseText: 'OK', + }); + + const response = await responsePromise; + expect(response.data).toBe('OK - modified by interceptor'); + }); + + it('should add a response interceptor that returns a new data object', async () => { + axios.interceptors.response.use(() => ({ + data: 'stuff', + })); + + const responsePromise = axios('/foo'); + const request = await waitForRequest(); + + request.respondWith({ + status: 200, + responseText: 'OK', + }); + + const response = await responsePromise; + expect(response.data).toBe('stuff'); + }); + + it('should add a response interceptor that returns a promise', async () => { + axios.interceptors.response.use((data) => + new Promise((resolve) => { + setTimeout(() => { + data.data = 'you have been promised!'; + resolve(data); + }, 10); + }) + ); + + const responsePromise = axios('/foo'); + const request = await waitForRequest(); + + request.respondWith({ + status: 200, + responseText: 'OK', + }); + + const response = await responsePromise; + expect(response.data).toBe('you have been promised!'); + }); + + describe('given multiple response interceptors', () => { + const fireRequest = async () => { + const responsePromise = axios('/foo'); + const request = await waitForRequest(); + + request.respondWith({ + status: 200, + responseText: 'OK', + }); + + return responsePromise; + }; + + it('then each interceptor is executed', async () => { + const interceptor1 = vi.fn((response) => response); + const interceptor2 = vi.fn((response) => response); + + axios.interceptors.response.use(interceptor1); + axios.interceptors.response.use(interceptor2); + + await fireRequest(); + + expect(interceptor1).toHaveBeenCalled(); + expect(interceptor2).toHaveBeenCalled(); + }); + + it('then they are executed in the order they were added', async () => { + const interceptor1 = vi.fn((response) => response); + const interceptor2 = vi.fn((response) => response); + + axios.interceptors.response.use(interceptor1); + axios.interceptors.response.use(interceptor2); + + await fireRequest(); + + expect(interceptor1.mock.invocationCallOrder[0]).toBeLessThan(interceptor2.mock.invocationCallOrder[0]); + }); + + it("then only the last interceptor's result is returned", async () => { + axios.interceptors.response.use(() => 'response 1'); + axios.interceptors.response.use(() => 'response 2'); + + const response = await fireRequest(); + expect(response).toBe('response 2'); + }); + + it("then every interceptor receives the result of its predecessor", async () => { + axios.interceptors.response.use(() => 'response 1'); + axios.interceptors.response.use((response) => [response, 'response 2']); + + const response = await fireRequest(); + expect(response).toEqual(['response 1', 'response 2']); + }); + + describe('and when the fulfillment interceptor throws', () => { + const fireRequestCatch = async () => { + const responsePromise = axios('/foo').catch(() => {}); + const request = await waitForRequest(); + + request.respondWith({ + status: 200, + responseText: 'OK', + }); + + await responsePromise; + }; + + it('then the following fulfillment interceptor is not called', async () => { + axios.interceptors.response.use(() => { + throw new Error('throwing interceptor'); + }); + + const interceptor2 = vi.fn((response) => response); + axios.interceptors.response.use(interceptor2); + + await fireRequestCatch(); + expect(interceptor2).not.toHaveBeenCalled(); + }); + + it('then the following rejection interceptor is called', async () => { + axios.interceptors.response.use(() => { + throw new Error('throwing interceptor'); + }); + + const rejectIntercept = vi.fn((error) => Promise.reject(error)); + axios.interceptors.response.use(() => {}, rejectIntercept); + + await fireRequestCatch(); + expect(rejectIntercept).toHaveBeenCalled(); + }); + + it('once caught, another following fulfillment interceptor is called again', async () => { + axios.interceptors.response.use(() => { + throw new Error('throwing interceptor'); + }); + + axios.interceptors.response.use( + () => {}, + () => 'recovered' + ); + + const interceptor3 = vi.fn((response) => response); + axios.interceptors.response.use(interceptor3); + + await fireRequestCatch(); + expect(interceptor3).toHaveBeenCalled(); + }); + }); + }); + + it('should allow removing interceptors', async () => { + axios.interceptors.response.use((data) => { + data.data = `${data.data}1`; + return data; + }); + const intercept = axios.interceptors.response.use((data) => { + data.data = `${data.data}2`; + return data; + }); + axios.interceptors.response.use((data) => { + data.data = `${data.data}3`; + return data; + }); + + axios.interceptors.response.eject(intercept); + + const responsePromise = axios('/foo'); + const request = await waitForRequest(); + + request.respondWith({ + status: 200, + responseText: 'OK', + }); + + const response = await responsePromise; + expect(response.data).toBe('OK13'); + }); + + it('should remove async interceptor before making request and execute synchronously', async () => { + let asyncFlag = false; + + const asyncIntercept = axios.interceptors.request.use( + (config) => { + config.headers.async = 'async it!'; + return config; + }, + null, + { synchronous: false } + ); + + axios.interceptors.request.use( + (config) => { + config.headers.sync = 'hello world'; + expect(asyncFlag).toBe(false); + return config; + }, + null, + { synchronous: true } + ); + + axios.interceptors.request.eject(asyncIntercept); + + const responsePromise = axios('/foo'); + asyncFlag = true; + + const request = await waitForRequest(); + expect(request.requestHeaders.async).toBeUndefined(); + expect(request.requestHeaders.sync).toBe('hello world'); + request.respondWith(); + await responsePromise; + }); + + it('should execute interceptors before transformers', async () => { + axios.interceptors.request.use((config) => { + config.data.baz = 'qux'; + return config; + }); + + const responsePromise = axios.post('/foo', { + foo: 'bar', + }); + + const request = await waitForRequest(); + expect(request.params).toEqual('{"foo":"bar","baz":"qux"}'); + request.respondWith(); + await responsePromise; + }); + + it('should modify base URL in request interceptor', async () => { + const instance = axios.create({ + baseURL: 'http://test.com/', + }); + + instance.interceptors.request.use((config) => { + config.baseURL = 'http://rebase.com/'; + return config; + }); + + const responsePromise = instance.get('/foo'); + const request = await waitForRequest(); + + expect(request.url).toBe('http://rebase.com/foo'); + request.respondWith(); + await responsePromise; + }); + + it('should clear all request interceptors', () => { + const instance = axios.create({ + baseURL: 'http://test.com/', + }); + + instance.interceptors.request.use((config) => config); + instance.interceptors.request.clear(); + + expect(instance.interceptors.request.handlers.length).toBe(0); + }); + + it('should clear all response interceptors', () => { + const instance = axios.create({ + baseURL: 'http://test.com/', + }); + + instance.interceptors.response.use((config) => config); + instance.interceptors.response.clear(); + + expect(instance.interceptors.response.handlers.length).toBe(0); + }); +}); diff --git a/tests/browser/isURLSameOrigin.browser.test.js b/tests/browser/isURLSameOrigin.browser.test.js new file mode 100644 index 00000000..032fba86 --- /dev/null +++ b/tests/browser/isURLSameOrigin.browser.test.js @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; + +import isURLSameOrigin from '../../lib/helpers/isURLSameOrigin.js'; + +describe('helpers::isURLSameOrigin (vitest browser)', () => { + it('detects same origin', () => { + expect(isURLSameOrigin(window.location.href)).toBe(true); + }); + + it('detects different origin', () => { + expect(isURLSameOrigin('https://github.com/axios/axios')).toBe(false); + }); +}); diff --git a/tests/browser/options.browser.test.js b/tests/browser/options.browser.test.js new file mode 100644 index 00000000..8b9ed6dc --- /dev/null +++ b/tests/browser/options.browser.test.js @@ -0,0 +1,220 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import axios from '../../index.js'; + +class MockXMLHttpRequest { + constructor() { + this.requestHeaders = {}; + this.responseHeaders = ''; + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.response = null; + this.onreadystatechange = null; + this.onloadend = null; + this.upload = { + addEventListener() {}, + }; + } + + open(method, url, async = true) { + this.method = method; + this.url = url; + this.async = async; + } + + setRequestHeader(key, value) { + this.requestHeaders[key] = value; + } + + addEventListener() {} + + getAllResponseHeaders() { + return this.responseHeaders; + } + + send(data) { + this.params = data; + requests.push(this); + } + + respondWith({ status = 200, statusText = 'OK', responseText = '', responseHeaders = '' } = {}) { + this.status = status; + this.statusText = statusText; + this.responseText = responseText; + this.response = responseText; + this.responseHeaders = responseHeaders; + this.readyState = 4; + + queueMicrotask(() => { + if (this.onloadend) { + this.onloadend(); + } else if (this.onreadystatechange) { + this.onreadystatechange(); + } + }); + } +} + +let requests = []; +let OriginalXMLHttpRequest; + +const startRequest = (...args) => { + const promise = axios(...args); + const request = requests.at(-1); + + expect(request).toBeDefined(); + + return { request, promise }; +}; + +const flushSuccess = async (request, promise) => { + request.respondWith({ status: 200 }); + await promise; +}; + +describe('options (vitest browser)', () => { + beforeEach(() => { + requests = []; + OriginalXMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(() => { + window.XMLHttpRequest = OriginalXMLHttpRequest; + vi.restoreAllMocks(); + }); + + it('should default method to get', async () => { + const { request, promise } = startRequest('/foo'); + + expect(request.method).toBe('GET'); + + await flushSuccess(request, promise); + }); + + it('should accept headers', async () => { + const { request, promise } = startRequest('/foo', { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + + expect(request.requestHeaders['X-Requested-With']).toBe('XMLHttpRequest'); + + await flushSuccess(request, promise); + }); + + it('should accept params', async () => { + const { request, promise } = startRequest('/foo', { + params: { + foo: 123, + bar: 456, + }, + }); + + expect(request.url).toBe('/foo?foo=123&bar=456'); + + await flushSuccess(request, promise); + }); + + it('should allow overriding default headers', async () => { + const { request, promise } = startRequest('/foo', { + headers: { + Accept: 'foo/bar', + }, + }); + + expect(request.requestHeaders.Accept).toBe('foo/bar'); + + await flushSuccess(request, promise); + }); + + it('should accept base URL', async () => { + const instance = axios.create({ + baseURL: 'http://test.com/', + }); + + const promise = instance.get('/foo'); + const request = requests.at(-1); + + expect(request).toBeDefined(); + expect(request.url).toBe('http://test.com/foo'); + + await flushSuccess(request, promise); + }); + + it('should warn about baseUrl', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const instance = axios.create({ + baseUrl: 'http://example.com/', + }); + + const promise = instance.get('/foo'); + const request = requests.at(-1); + + expect(request).toBeDefined(); + expect(warnSpy).toHaveBeenCalledWith('baseUrl is likely a misspelling of baseURL'); + expect(request.url).toBe('/foo'); + + await flushSuccess(request, promise); + }); + + it('should ignore base URL if request URL is absolute', async () => { + const instance = axios.create({ + baseURL: 'http://someurl.com/', + }); + + const promise = instance.get('http://someotherurl.com/'); + const request = requests.at(-1); + + expect(request).toBeDefined(); + expect(request.url).toBe('http://someotherurl.com/'); + + await flushSuccess(request, promise); + }); + + it('should combine the URLs if base url and request url exist and allowAbsoluteUrls is false', async () => { + const instance = axios.create({ + baseURL: 'http://someurl.com/', + allowAbsoluteUrls: false, + }); + + const promise = instance.get('http://someotherurl.com/'); + const request = requests.at(-1); + + expect(request).toBeDefined(); + expect(request.url).toBe('http://someurl.com/http://someotherurl.com/'); + + await flushSuccess(request, promise); + }); + + it('should change only the baseURL of the specified instance', () => { + const instance1 = axios.create(); + const instance2 = axios.create(); + + instance1.defaults.baseURL = 'http://instance1.example.com/'; + + expect(instance2.defaults.baseURL).not.toBe('http://instance1.example.com/'); + }); + + it('should change only the headers of the specified instance', () => { + const instance1 = axios.create(); + const instance2 = axios.create(); + + instance1.defaults.headers.common.Authorization = 'faketoken'; + instance2.defaults.headers.common.Authorization = 'differentfaketoken'; + + instance1.defaults.headers.common['Content-Type'] = 'application/xml'; + instance2.defaults.headers.common['Content-Type'] = 'application/x-www-form-urlencoded'; + + expect(axios.defaults.headers.common.Authorization).toBeUndefined(); + expect(instance1.defaults.headers.common.Authorization).toBe('faketoken'); + expect(instance2.defaults.headers.common.Authorization).toBe('differentfaketoken'); + + expect(axios.defaults.headers.common['Content-Type']).toBeUndefined(); + expect(instance1.defaults.headers.common['Content-Type']).toBe('application/xml'); + expect(instance2.defaults.headers.common['Content-Type']).toBe('application/x-www-form-urlencoded'); + }); +}); diff --git a/tests/browser/progress.browser.test.js b/tests/browser/progress.browser.test.js new file mode 100644 index 00000000..2a7fc412 --- /dev/null +++ b/tests/browser/progress.browser.test.js @@ -0,0 +1,230 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import axios from '../../index.js'; + +class MockXMLHttpRequest { + constructor() { + this.requestHeaders = {}; + this.responseHeaders = {}; + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.response = null; + this.timeout = 0; + this.withCredentials = false; + this.onreadystatechange = null; + this.onloadend = null; + this.onabort = null; + this.onerror = null; + this.ontimeout = null; + this._listeners = {}; + this._uploadListeners = {}; + this.upload = { + addEventListener: (type, listener) => { + this._uploadListeners[type] ||= []; + this._uploadListeners[type].push(listener); + }, + }; + } + + open(method, url, async = true) { + this.method = method; + this.url = url; + this.async = async; + } + + setRequestHeader(key, value) { + this.requestHeaders[key] = value; + } + + addEventListener(type, listener) { + this._listeners[type] ||= []; + this._listeners[type].push(listener); + } + + getAllResponseHeaders() { + return Object.entries(this.responseHeaders) + .map(([key, value]) => `${key}: ${value}`) + .join('\n'); + } + + send(data) { + this.params = data; + this.readyState = 1; + requests.push(this); + } + + getListenerCount(type, target = 'request') { + const listeners = target === 'upload' ? this._uploadListeners : this._listeners; + return listeners[type]?.length || 0; + } + + emit(type, target = 'request', event = {}) { + const listeners = target === 'upload' ? this._uploadListeners : this._listeners; + (listeners[type] || []).forEach((listener) => listener(event)); + } + + respondWith({ + status = 200, + statusText = 'OK', + responseText = '', + response = null, + headers = {}, + } = {}) { + this.status = status; + this.statusText = statusText; + this.responseText = responseText; + this.response = response; + this.responseHeaders = headers; + this.readyState = 4; + + this.emit('progress', 'request', { + loaded: responseText.length, + total: responseText.length, + lengthComputable: true, + }); + + queueMicrotask(() => { + if (this.onloadend) { + this.onloadend(); + } else if (this.onreadystatechange) { + this.onreadystatechange(); + } + }); + } +} + +let requests = []; +let OriginalXMLHttpRequest; + +const getLastRequest = () => { + const request = requests.at(-1); + + expect(request).toBeDefined(); + + return request; +}; + +describe('progress (vitest browser)', () => { + beforeEach(() => { + requests = []; + OriginalXMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(() => { + window.XMLHttpRequest = OriginalXMLHttpRequest; + vi.restoreAllMocks(); + }); + + it('should add a download progress handler', async () => { + const progressSpy = vi.fn(); + const responsePromise = axios('/foo', { onDownloadProgress: progressSpy }); + const request = getLastRequest(); + + request.respondWith({ + status: 200, + responseText: '{"foo": "bar"}', + }); + await responsePromise; + + expect(progressSpy).toHaveBeenCalled(); + }); + + it('should add an upload progress handler', async () => { + const progressSpy = vi.fn(); + const responsePromise = axios('/foo', { onUploadProgress: progressSpy }); + const request = getLastRequest(); + + expect(request.getListenerCount('progress', 'upload')).toBe(1); + + request.respondWith({ + status: 200, + responseText: '{"foo": "bar"}', + }); + await responsePromise; + }); + + it('should add both upload and download progress handlers', async () => { + const downloadProgressSpy = vi.fn(); + const uploadProgressSpy = vi.fn(); + const responsePromise = axios('/foo', { + onDownloadProgress: downloadProgressSpy, + onUploadProgress: uploadProgressSpy, + }); + const request = getLastRequest(); + + expect(downloadProgressSpy).not.toHaveBeenCalled(); + expect(request.getListenerCount('progress', 'request')).toBe(1); + expect(request.getListenerCount('progress', 'upload')).toBe(1); + + request.respondWith({ + status: 200, + responseText: '{"foo": "bar"}', + }); + await responsePromise; + + expect(downloadProgressSpy).toHaveBeenCalled(); + }); + + it('should add a download progress handler from instance config', async () => { + const progressSpy = vi.fn(); + const instance = axios.create({ + onDownloadProgress: progressSpy, + }); + + const responsePromise = instance.get('/foo'); + const request = getLastRequest(); + + request.respondWith({ + status: 200, + responseText: '{"foo": "bar"}', + }); + await responsePromise; + + expect(progressSpy).toHaveBeenCalled(); + }); + + it('should add an upload progress handler from instance config', async () => { + const progressSpy = vi.fn(); + const instance = axios.create({ + onUploadProgress: progressSpy, + }); + + const responsePromise = instance.get('/foo'); + const request = getLastRequest(); + + expect(request.getListenerCount('progress', 'upload')).toBe(1); + + request.respondWith({ + status: 200, + responseText: '{"foo": "bar"}', + }); + await responsePromise; + }); + + it('should add upload and download progress handlers from instance config', async () => { + const downloadProgressSpy = vi.fn(); + const uploadProgressSpy = vi.fn(); + const instance = axios.create({ + onDownloadProgress: downloadProgressSpy, + onUploadProgress: uploadProgressSpy, + }); + + const responsePromise = instance.get('/foo'); + const request = getLastRequest(); + + expect(downloadProgressSpy).not.toHaveBeenCalled(); + expect(request.getListenerCount('progress', 'request')).toBe(1); + expect(request.getListenerCount('progress', 'upload')).toBe(1); + + request.respondWith({ + status: 200, + responseText: '{"foo": "bar"}', + }); + await responsePromise; + + expect(downloadProgressSpy).toHaveBeenCalled(); + }); +}); diff --git a/tests/browser/promise.browser.test.js b/tests/browser/promise.browser.test.js new file mode 100644 index 00000000..ba9efa3c --- /dev/null +++ b/tests/browser/promise.browser.test.js @@ -0,0 +1,122 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import axios from '../../index.js'; + +class MockXMLHttpRequest { + constructor() { + this.requestHeaders = {}; + this.responseHeaders = ''; + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.response = null; + this.onreadystatechange = null; + this.onloadend = null; + this.upload = { + addEventListener() {}, + }; + } + + open(method, url, async = true) { + this.method = method; + this.url = url; + this.async = async; + } + + setRequestHeader(key, value) { + this.requestHeaders[key] = value; + } + + addEventListener() {} + + getAllResponseHeaders() { + return this.responseHeaders; + } + + send(data) { + this.params = data; + requests.push(this); + } + + respondWith({ status = 200, statusText = 'OK', responseText = '', responseHeaders = '' } = {}) { + this.status = status; + this.statusText = statusText; + this.responseText = responseText; + this.response = responseText; + this.responseHeaders = responseHeaders; + this.readyState = 4; + + queueMicrotask(() => { + if (this.onloadend) { + this.onloadend(); + } else if (this.onreadystatechange) { + this.onreadystatechange(); + } + }); + } + + abort() {} +} + +let requests = []; +let OriginalXMLHttpRequest; + +const getLastRequest = () => { + const request = requests.at(-1); + + expect(request).toBeDefined(); + + return request; +}; + +describe('promise (vitest browser)', () => { + beforeEach(() => { + requests = []; + OriginalXMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(() => { + window.XMLHttpRequest = OriginalXMLHttpRequest; + }); + + it('should provide succinct object to then', async () => { + const responsePromise = axios('/foo'); + const request = getLastRequest(); + + request.respondWith({ + status: 200, + responseText: '{"hello":"world"}', + responseHeaders: 'Content-Type: application/json', + }); + + const response = await responsePromise; + + expect(typeof response).toBe('object'); + expect(response.data.hello).toBe('world'); + expect(response.status).toBe(200); + expect(response.headers['content-type']).toBe('application/json'); + expect(response.config.url).toBe('/foo'); + }); + + it('should support all', async () => { + const result = await axios.all([true, 123]); + + expect(result).toEqual([true, 123]); + }); + + it('should support spread', async () => { + let fulfilled = false; + const result = await axios.all([123, 456]).then( + axios.spread((a, b) => { + expect(a + b).toBe(123 + 456); + fulfilled = true; + return 'hello world'; + }) + ); + + expect(fulfilled).toBe(true); + expect(result).toBe('hello world'); + }); +}); diff --git a/tests/browser/requests.browser.test.js b/tests/browser/requests.browser.test.js new file mode 100644 index 00000000..ab76dd38 --- /dev/null +++ b/tests/browser/requests.browser.test.js @@ -0,0 +1,497 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import axios from '../../index.js'; + +class MockXMLHttpRequest { + constructor() { + this.requestHeaders = {}; + this.responseHeaders = {}; + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.response = null; + this.responseURL = ''; + this.timeout = 0; + this.withCredentials = false; + this.onreadystatechange = null; + this.onloadend = null; + this.onabort = null; + this.onerror = null; + this.ontimeout = null; + this.upload = { + addEventListener() {}, + }; + } + + open(method, url, async = true) { + this.method = method; + this.url = url; + this.async = async; + } + + setRequestHeader(key, value) { + this.requestHeaders[key] = value; + } + + addEventListener() {} + + getAllResponseHeaders() { + return Object.entries(this.responseHeaders) + .map(([key, value]) => `${key}: ${value}`) + .join('\n'); + } + + send(data) { + this.params = data; + this.readyState = 1; + requests.push(this); + } + + respondWith({ + status = 200, + statusText = 'OK', + responseText = '', + response = null, + headers = {}, + responseURL = '', + } = {}) { + this.status = status; + this.statusText = statusText; + this.responseText = responseText; + this.response = response; + this.responseHeaders = headers; + this.responseURL = responseURL; + this.readyState = 4; + this.finish(); + } + + responseTimeout() { + if (this.ontimeout) { + this.ontimeout(); + } + } + + failNetworkError(message = 'Network Error') { + if (this.onerror) { + this.onerror({ message }); + } + } + + abort() { + if (this.onabort) { + this.onabort(); + } + } + + finish() { + queueMicrotask(() => { + if (this.onloadend) { + this.onloadend(); + } else if (this.onreadystatechange) { + this.onreadystatechange(); + } + }); + } +} + +let requests = []; +let OriginalXMLHttpRequest; + +const startRequest = (...args) => { + const promise = axios(...args); + const request = requests.at(-1); + expect(request).toBeDefined(); + + return { request, promise }; +}; + +const flushSuccess = async (request, promise) => { + request.respondWith({ status: 200 }); + await promise; +}; + +describe('requests (vitest browser)', () => { + beforeEach(() => { + requests = []; + OriginalXMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(() => { + window.XMLHttpRequest = OriginalXMLHttpRequest; + vi.restoreAllMocks(); + }); + + it('should treat single string arg as url', async () => { + const { request, promise } = startRequest('/foo'); + + expect(request.url).toBe('/foo'); + expect(request.method).toBe('GET'); + + await flushSuccess(request, promise); + }); + + it('should treat method value as lowercase string', async () => { + const { request, promise } = startRequest({ + url: '/foo', + method: 'POST', + }); + + request.respondWith({ status: 200 }); + const response = await promise; + + expect(response.config.method).toBe('post'); + }); + + it('should allow string arg as url, and config arg', async () => { + const { request, promise } = startRequest('/foo', { + method: 'post', + }); + + expect(request.url).toBe('/foo'); + expect(request.method).toBe('POST'); + + await flushSuccess(request, promise); + }); + + it('should allow data', async () => { + const { request, promise } = startRequest('/foo', { + method: 'delete', + data: { foo: 'bar' }, + }); + + expect(request.params).toBe(JSON.stringify({ foo: 'bar' })); + + await flushSuccess(request, promise); + }); + + it('should make an http request', async () => { + const { request, promise } = startRequest('/foo'); + + expect(request.url).toBe('/foo'); + + await flushSuccess(request, promise); + }); + + describe('timeouts', () => { + it('should handle timeouts', async () => { + const { request, promise } = startRequest({ + url: '/foo', + timeout: 100, + }); + + request.responseTimeout(); + + const err = await promise.catch((error) => error); + + expect(err).toBeInstanceOf(Error); + expect(err.code).toBe('ECONNABORTED'); + }); + + describe('transitional.clarifyTimeoutError', () => { + it('should throw ETIMEDOUT instead of ECONNABORTED on request timeouts', async () => { + const { request, promise } = startRequest({ + url: '/foo', + timeout: 100, + transitional: { + clarifyTimeoutError: true, + }, + }); + + request.responseTimeout(); + + const err = await promise.catch((error) => error); + + expect(err).toBeInstanceOf(Error); + expect(err.code).toBe('ETIMEDOUT'); + }); + }); + }); + + it('should reject on network errors', async () => { + const { request, promise } = startRequest('http://thisisnotaserver/foo'); + + request.failNetworkError(); + + const reason = await promise.catch((error) => error); + + expect(reason).toBeInstanceOf(Error); + expect(reason.config.method).toBe('get'); + expect(reason.config.url).toBe('http://thisisnotaserver/foo'); + expect(reason.request).toBeInstanceOf(MockXMLHttpRequest); + }); + + it('should reject on abort', async () => { + const { request, promise } = startRequest('/foo'); + + request.abort(); + + const reason = await promise.catch((error) => error); + + expect(reason).toBeInstanceOf(Error); + expect(reason.config.method).toBe('get'); + expect(reason.config.url).toBe('/foo'); + expect(reason.request).toBeInstanceOf(MockXMLHttpRequest); + }); + + it('should reject when validateStatus returns false', async () => { + const { request, promise } = startRequest('/foo', { + validateStatus(status) { + return status !== 500; + }, + }); + + request.respondWith({ status: 500 }); + const reason = await promise.catch((error) => error); + + expect(reason).toBeInstanceOf(Error); + expect(reason.message).toBe('Request failed with status code 500'); + expect(reason.config.method).toBe('get'); + expect(reason.config.url).toBe('/foo'); + expect(reason.response.status).toBe(500); + }); + + it('should resolve when validateStatus returns true', async () => { + const { request, promise } = startRequest('/foo', { + validateStatus(status) { + return status === 500; + }, + }); + + request.respondWith({ status: 500 }); + await expect(promise).resolves.toBeDefined(); + }); + + it('should resolve when the response status is 0 (file protocol)', async () => { + const { request, promise } = startRequest('file:///xxx'); + + request.respondWith({ + status: 0, + responseURL: 'file:///xxx', + }); + + await expect(promise).resolves.toBeDefined(); + }); + + it('should resolve when validateStatus is null', async () => { + const { request, promise } = startRequest('/foo', { + validateStatus: null, + }); + + request.respondWith({ status: 500 }); + await expect(promise).resolves.toBeDefined(); + }); + + it('should resolve when validateStatus is undefined', async () => { + const { request, promise } = startRequest('/foo', { + validateStatus: undefined, + }); + + request.respondWith({ status: 500 }); + await expect(promise).resolves.toBeDefined(); + }); + + // https://github.com/axios/axios/issues/378 + it('should return JSON when rejecting', async () => { + const { request, promise } = startRequest( + '/api/account/signup', + { + username: null, + password: null, + }, + { + method: 'post', + headers: { + Accept: 'application/json', + }, + } + ); + + request.respondWith({ + status: 400, + statusText: 'Bad Request', + responseText: '{"error": "BAD USERNAME", "code": 1}', + }); + + const error = await promise.catch((err) => err); + const response = error.response; + + expect(typeof response.data).toBe('object'); + expect(response.data.error).toBe('BAD USERNAME'); + expect(response.data.code).toBe(1); + }); + + it('should make cross domain http request', async () => { + const { request, promise } = startRequest('www.someurl.com/foo', { + method: 'post', + }); + + request.respondWith({ + status: 200, + statusText: 'OK', + responseText: '{"foo": "bar"}', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const response = await promise; + + expect(response.data.foo).toBe('bar'); + expect(response.status).toBe(200); + expect(response.statusText).toBe('OK'); + expect(response.headers['content-type']).toBe('application/json'); + }); + + it('should supply correct response', async () => { + const { request, promise } = startRequest('/foo', { + method: 'post', + }); + + request.respondWith({ + status: 200, + statusText: 'OK', + responseText: '{"foo": "bar"}', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const response = await promise; + + expect(response.data.foo).toBe('bar'); + expect(response.status).toBe(200); + expect(response.statusText).toBe('OK'); + expect(response.headers['content-type']).toBe('application/json'); + }); + + it('should not modify the config url with relative baseURL', async () => { + const { request, promise } = startRequest('/foo', { + baseURL: '/api', + }); + + request.respondWith({ + status: 404, + statusText: 'NOT FOUND', + responseText: 'Resource not found', + }); + + const error = await promise.catch((err) => err); + const config = error.config; + + expect(config.baseURL).toBe('/api'); + expect(config.url).toBe('/foo'); + }); + + it('should allow overriding Content-Type header case-insensitive', async () => { + const contentType = 'application/vnd.myapp.type+json'; + const { request, promise } = startRequest('/foo', { + method: 'post', + data: { prop: 'value' }, + headers: { + 'Content-Type': contentType, + }, + }); + + expect(request.requestHeaders['Content-Type']).toBe(contentType); + await flushSuccess(request, promise); + }); + + it('should support binary data as array buffer', async () => { + const input = new Int8Array([1, 2]); + const { request, promise } = startRequest('/foo', { + method: 'post', + data: input.buffer, + }); + + const output = new Int8Array(request.params); + expect(output.length).toBe(2); + expect(output[0]).toBe(1); + expect(output[1]).toBe(2); + + await flushSuccess(request, promise); + }); + + it('should support binary data as array buffer view', async () => { + const input = new Int8Array([1, 2]); + const { request, promise } = startRequest('/foo', { + method: 'post', + data: input, + }); + + const output = new Int8Array(request.params); + expect(output.length).toBe(2); + expect(output[0]).toBe(1); + expect(output[1]).toBe(2); + + await flushSuccess(request, promise); + }); + + it('should support array buffer response', async () => { + const str2ab = (str) => { + const buff = new ArrayBuffer(str.length * 2); + const view = new Uint16Array(buff); + + for (let i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + + return buff; + }; + + const { request, promise } = startRequest('/foo', { + responseType: 'arraybuffer', + }); + + request.respondWith({ + status: 200, + response: str2ab('Hello world'), + }); + + const response = await promise; + expect(response.data.byteLength).toBe(22); + }); + + it('should support URLSearchParams', async () => { + const params = new URLSearchParams(); + params.append('param1', 'value1'); + params.append('param2', 'value2'); + + const { request, promise } = startRequest('/foo', { + method: 'post', + data: params, + }); + + expect(request.requestHeaders['Content-Type']).toBe( + 'application/x-www-form-urlencoded;charset=utf-8' + ); + expect(request.params).toBe('param1=value1¶m2=value2'); + + await flushSuccess(request, promise); + }); + + it('should support HTTP protocol', async () => { + const { request, promise } = startRequest('/foo', { + method: 'get', + }); + + expect(request.method).toBe('GET'); + await flushSuccess(request, promise); + }); + + it('should support HTTPS protocol', async () => { + const { request, promise } = startRequest('https://www.google.com', { + method: 'get', + }); + + expect(request.method).toBe('GET'); + await flushSuccess(request, promise); + }); + + it('should return unsupported protocol error message', async () => { + await expect(axios.get('ftp:localhost')).rejects.toMatchObject({ + message: 'Unsupported protocol ftp:', + }); + }); +}); diff --git a/tests/browser/settle.browser.test.js b/tests/browser/settle.browser.test.js new file mode 100644 index 00000000..3bd39a2b --- /dev/null +++ b/tests/browser/settle.browser.test.js @@ -0,0 +1,99 @@ +import { describe, expect, it, vi } from 'vitest'; + +import settle from '../../lib/core/settle.js'; +import AxiosError from '../../lib/core/AxiosError.js'; + +describe('core::settle (vitest browser)', () => { + it('resolves when response status is missing', () => { + const resolve = vi.fn(); + const reject = vi.fn(); + const response = { + config: { + validateStatus: () => true, + }, + }; + + settle(resolve, reject, response); + + expect(resolve).toHaveBeenCalledOnce(); + expect(resolve).toHaveBeenCalledWith(response); + expect(reject).not.toHaveBeenCalled(); + }); + + it('resolves when validateStatus is not configured', () => { + const resolve = vi.fn(); + const reject = vi.fn(); + const response = { + status: 500, + config: {}, + }; + + settle(resolve, reject, response); + + expect(resolve).toHaveBeenCalledOnce(); + expect(resolve).toHaveBeenCalledWith(response); + expect(reject).not.toHaveBeenCalled(); + }); + + it('resolves when validateStatus returns true', () => { + const resolve = vi.fn(); + const reject = vi.fn(); + const response = { + status: 500, + config: { + validateStatus: () => true, + }, + }; + + settle(resolve, reject, response); + + expect(resolve).toHaveBeenCalledOnce(); + expect(resolve).toHaveBeenCalledWith(response); + expect(reject).not.toHaveBeenCalled(); + }); + + it('rejects with an AxiosError when validateStatus returns false', () => { + const resolve = vi.fn(); + const reject = vi.fn(); + const request = { + path: '/foo', + }; + const response = { + status: 500, + config: { + validateStatus: () => false, + }, + request, + }; + + settle(resolve, reject, response); + + expect(resolve).not.toHaveBeenCalled(); + expect(reject).toHaveBeenCalledOnce(); + + const reason = reject.mock.calls[0][0]; + expect(reason).toBeInstanceOf(AxiosError); + expect(reason.message).toBe('Request failed with status code 500'); + expect(reason.code).toBe(AxiosError.ERR_BAD_RESPONSE); + expect(reason.config).toBe(response.config); + expect(reason.request).toBe(request); + expect(reason.response).toBe(response); + }); + + it('passes response status to validateStatus', () => { + const resolve = vi.fn(); + const reject = vi.fn(); + const validateStatus = vi.fn(); + const response = { + status: 500, + config: { + validateStatus, + }, + }; + + settle(resolve, reject, response); + + expect(validateStatus).toHaveBeenCalledOnce(); + expect(validateStatus).toHaveBeenCalledWith(500); + }); +}); diff --git a/tests/browser/smoke.browser.test.js b/tests/browser/smoke.browser.test.js deleted file mode 100644 index d046e787..00000000 --- a/tests/browser/smoke.browser.test.js +++ /dev/null @@ -1,10 +0,0 @@ -import { expect, test } from 'vitest'; - -test('runs in browser environment', () => { - document.body.innerHTML = '
vitest browser smoke
'; - - const el = document.querySelector('[data-testid="smoke"]'); - - expect(el?.textContent).toBe('vitest browser smoke'); - expect(globalThis.window).toBeDefined(); -}); diff --git a/tests/browser/toFormData.browser.test.js b/tests/browser/toFormData.browser.test.js new file mode 100644 index 00000000..07fd0842 --- /dev/null +++ b/tests/browser/toFormData.browser.test.js @@ -0,0 +1,121 @@ +import { describe, expect, it } from 'vitest'; + +import toFormData from '../../lib/helpers/toFormData.js'; + +describe('helpers::toFormData (vitest browser)', () => { + it('converts nested data object to FormData with dots option enabled', () => { + const data = { + val: 123, + nested: { + arr: ['hello', 'world'], + }, + }; + + const form = toFormData(data, null, { dots: true }); + + expect(form).toBeInstanceOf(FormData); + expect(Array.from(form.keys())).toHaveLength(3); + expect(form.get('val')).toBe('123'); + expect(form.get('nested.arr.0')).toBe('hello'); + }); + + it('respects metaTokens option', () => { + const data = { + 'obj{}': { x: 1, y: 2 }, + }; + const serialized = JSON.stringify(data['obj{}']); + + const form = toFormData(data, null, { metaTokens: false }); + + expect(Array.from(form.keys())).toHaveLength(1); + expect(form.getAll('obj')).toEqual([serialized]); + }); + + describe('flat arrays serialization', () => { + it('includes full indexes when indexes option is true', () => { + const data = { + arr: [1, 2, 3], + arr2: [1, [2], 3], + }; + + const form = toFormData(data, null, { indexes: true }); + + expect(Array.from(form.keys())).toHaveLength(6); + expect(form.get('arr[0]')).toBe('1'); + expect(form.get('arr[1]')).toBe('2'); + expect(form.get('arr[2]')).toBe('3'); + expect(form.get('arr2[0]')).toBe('1'); + expect(form.get('arr2[1][0]')).toBe('2'); + expect(form.get('arr2[2]')).toBe('3'); + }); + + it('includes brackets only when indexes option is false', () => { + const data = { + arr: [1, 2, 3], + arr2: [1, [2], 3], + }; + + const form = toFormData(data, null, { indexes: false }); + + expect(Array.from(form.keys())).toHaveLength(6); + expect(form.getAll('arr[]')).toEqual(['1', '2', '3']); + expect(form.get('arr2[0]')).toBe('1'); + expect(form.get('arr2[1][0]')).toBe('2'); + expect(form.get('arr2[2]')).toBe('3'); + }); + + it('omits brackets when indexes option is null', () => { + const data = { + arr: [1, 2, 3], + arr2: [1, [2], 3], + }; + + const form = toFormData(data, null, { indexes: null }); + + expect(Array.from(form.keys())).toHaveLength(6); + expect(form.getAll('arr')).toEqual(['1', '2', '3']); + expect(form.get('arr2[0]')).toBe('1'); + expect(form.get('arr2[1][0]')).toBe('2'); + expect(form.get('arr2[2]')).toBe('3'); + }); + }); + + it('converts nested data object to FormData', () => { + const data = { + val: 123, + nested: { + arr: ['hello', 'world'], + }, + }; + + const form = toFormData(data); + + expect(form).toBeInstanceOf(FormData); + expect(Array.from(form.keys())).toHaveLength(3); + expect(form.get('val')).toBe('123'); + expect(form.get('nested[arr][0]')).toBe('hello'); + }); + + it('appends value whose key ends with [] as separate values with the same key', () => { + const data = { + 'arr[]': [1, 2, 3], + }; + + const form = toFormData(data); + + expect(Array.from(form.keys())).toHaveLength(3); + expect(form.getAll('arr[]')).toEqual(['1', '2', '3']); + }); + + it('appends value whose key ends with {} as a JSON string', () => { + const data = { + 'obj{}': { x: 1, y: 2 }, + }; + const serialized = JSON.stringify(data['obj{}']); + + const form = toFormData(data); + + expect(Array.from(form.keys())).toHaveLength(1); + expect(form.getAll('obj{}')).toEqual([serialized]); + }); +}); diff --git a/tests/browser/transform.browser.test.js b/tests/browser/transform.browser.test.js new file mode 100644 index 00000000..27a7d20f --- /dev/null +++ b/tests/browser/transform.browser.test.js @@ -0,0 +1,265 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import axios from '../../index.js'; +import AxiosError from '../../lib/core/AxiosError.js'; + +class MockXMLHttpRequest { + constructor() { + this.requestHeaders = {}; + this.responseHeaders = ''; + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.response = null; + this.timeout = 0; + this.onreadystatechange = null; + this.onloadend = null; + this.onabort = null; + this.onerror = null; + this.ontimeout = null; + this.upload = { + addEventListener() {}, + }; + } + + open(method, url, async = true) { + this.method = method; + this.url = url; + this.async = async; + } + + setRequestHeader(key, value) { + this.requestHeaders[key] = value; + } + + addEventListener() {} + + getAllResponseHeaders() { + return this.responseHeaders; + } + + send(data) { + this.params = data; + requests.push(this); + } + + respondWith({ status = 200, statusText = 'OK', responseText = '', responseHeaders = '' } = {}) { + this.status = status; + this.statusText = statusText; + this.responseText = responseText; + this.response = responseText; + this.responseHeaders = responseHeaders; + this.readyState = 4; + + queueMicrotask(() => { + if (this.onloadend) { + this.onloadend(); + } else if (this.onreadystatechange) { + this.onreadystatechange(); + } + }); + } + + abort() {} +} + +let requests = []; +let OriginalXMLHttpRequest; + +const getLastRequest = () => { + const request = requests.at(-1); + + expect(request).toBeDefined(); + + return request; +}; + +describe('transform (vitest browser)', () => { + beforeEach(() => { + requests = []; + OriginalXMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(() => { + window.XMLHttpRequest = OriginalXMLHttpRequest; + }); + + it('should transform JSON to string', async () => { + const responsePromise = axios.post('/foo', { foo: 'bar' }); + const request = getLastRequest(); + + expect(request.params).toBe('{"foo":"bar"}'); + + request.respondWith(); + await responsePromise; + }); + + it('should transform string to JSON', async () => { + const responsePromise = axios('/foo'); + const request = getLastRequest(); + + request.respondWith({ + status: 200, + responseText: '{"foo": "bar"}', + }); + + const response = await responsePromise; + + expect(typeof response.data).toBe('object'); + expect(response.data.foo).toBe('bar'); + }); + + it('should throw a SyntaxError if JSON parsing failed and responseType is "json" if silentJSONParsing is false', async () => { + const responsePromise = axios({ + url: '/foo', + responseType: 'json', + transitional: { silentJSONParsing: false }, + }); + const request = getLastRequest(); + + request.respondWith({ + status: 200, + responseText: '{foo": "bar"}', + }); + + const thrown = await responsePromise.catch((error) => error); + + expect(thrown).toBeTruthy(); + expect(thrown.name).toContain('SyntaxError'); + expect(thrown.code).toBe(AxiosError.ERR_BAD_RESPONSE); + }); + + it('should send data as JSON if request content-type is application/json', async () => { + const responsePromise = axios.post('/foo', 123, { + headers: { 'Content-Type': 'application/json' }, + }); + const request = getLastRequest(); + + request.respondWith({ + status: 200, + responseText: '', + }); + + const response = await responsePromise; + + expect(response).toBeTruthy(); + expect(request.requestHeaders['Content-Type']).toBe('application/json'); + expect(JSON.parse(request.params)).toBe(123); + }); + + it('should not assume JSON if responseType is not `json`', async () => { + const responsePromise = axios.get('/foo', { + responseType: 'text', + transitional: { + forcedJSONParsing: false, + }, + }); + const request = getLastRequest(); + const rawData = '{"x":1}'; + + request.respondWith({ + status: 200, + responseText: rawData, + }); + + const response = await responsePromise; + + expect(response).toBeTruthy(); + expect(response.data).toBe(rawData); + }); + + it('should override default transform', async () => { + const responsePromise = axios.post( + '/foo', + { foo: 'bar' }, + { + transformRequest(data) { + return data; + }, + } + ); + const request = getLastRequest(); + + expect(typeof request.params).toBe('object'); + + request.respondWith(); + await responsePromise; + }); + + it('should allow an Array of transformers', async () => { + const responsePromise = axios.post( + '/foo', + { foo: 'bar' }, + { + transformRequest: axios.defaults.transformRequest.concat(function (data) { + return data.replace('bar', 'baz'); + }), + } + ); + const request = getLastRequest(); + + expect(request.params).toBe('{"foo":"baz"}'); + + request.respondWith(); + await responsePromise; + }); + + it('should allowing mutating headers', async () => { + const token = Math.floor(Math.random() * Math.pow(2, 64)).toString(36); + const responsePromise = axios('/foo', { + transformRequest(data, headers) { + headers['X-Authorization'] = token; + return data; + }, + }); + const request = getLastRequest(); + + expect(request.requestHeaders['X-Authorization']).toBe(token); + + request.respondWith(); + await responsePromise; + }); + + it("should normalize 'content-type' header when using a custom transformRequest", async () => { + const responsePromise = axios.post( + '/foo', + { foo: 'bar' }, + { + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + transformRequest: [ + function () { + return 'aa=44'; + }, + ], + } + ); + const request = getLastRequest(); + + expect(request.requestHeaders['Content-Type']).toBe('application/x-www-form-urlencoded'); + + request.respondWith(); + await responsePromise; + }); + + it('should return response.data as parsed JSON object when responseType is json', async () => { + const instance = axios.create({ + baseURL: '/api', + responseType: 'json', + }); + const responsePromise = instance.get('my/endpoint', { responseType: 'json' }); + const request = getLastRequest(); + + request.respondWith({ + status: 200, + responseText: '{"key1": "value1"}', + responseHeaders: 'content-type: application/json', + }); + + const response = await responsePromise; + + expect(response).toBeTruthy(); + expect(response.data).toEqual({ key1: 'value1' }); + }); +}); diff --git a/tests/browser/xsrf.browser.test.js b/tests/browser/xsrf.browser.test.js new file mode 100644 index 00000000..027ab83b --- /dev/null +++ b/tests/browser/xsrf.browser.test.js @@ -0,0 +1,182 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import axios from '../../index.js'; +import cookies from '../../lib/helpers/cookies.js'; + +class MockXMLHttpRequest { + constructor() { + this.requestHeaders = {}; + this.readyState = 0; + this.status = 200; + this.statusText = 'OK'; + this.responseText = ''; + this.timeout = 0; + this.onreadystatechange = null; + this.onloadend = null; + this.onabort = null; + this.onerror = null; + this.ontimeout = null; + this.upload = { + addEventListener() {}, + }; + } + + open(method, url, async = true) { + this.method = method; + this.url = url; + this.async = async; + } + + setRequestHeader(key, value) { + this.requestHeaders[key] = value; + } + + addEventListener() {} + + getAllResponseHeaders() { + return ''; + } + + send() { + requests.push(this); + this.readyState = 4; + + queueMicrotask(() => { + if (this.onloadend) { + this.onloadend(); + } else if (this.onreadystatechange) { + this.onreadystatechange(); + } + }); + } + + abort() {} +} + +let requests = []; +let OriginalXMLHttpRequest; + +const setXsrfCookie = (value) => { + document.cookie = `${axios.defaults.xsrfCookieName}=${value}; path=/`; +}; + +const clearXsrfCookie = () => { + document.cookie = `${axios.defaults.xsrfCookieName}=; expires=${new Date( + Date.now() - 86400000 + ).toUTCString()}; path=/`; +}; + +const sendRequest = async (url, config) => { + const responsePromise = axios(url, config); + const request = requests.at(-1); + + expect(request).toBeDefined(); + await responsePromise; + + return request; +}; + +describe('xsrf (vitest browser)', () => { + beforeEach(() => { + requests = []; + OriginalXMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(() => { + clearXsrfCookie(); + window.XMLHttpRequest = OriginalXMLHttpRequest; + vi.restoreAllMocks(); + }); + + it('should not set xsrf header if cookie is null', async () => { + const request = await sendRequest('/foo'); + + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toBeUndefined(); + }); + + it('should set xsrf header if cookie is set', async () => { + setXsrfCookie('12345'); + + const request = await sendRequest('/foo'); + + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toBe('12345'); + }); + + it('should not set xsrf header if xsrfCookieName is null', async () => { + setXsrfCookie('12345'); + + const request = await sendRequest('/foo', { + xsrfCookieName: null, + }); + + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toBeUndefined(); + }); + + it('should not read cookies at all if xsrfCookieName is null', async () => { + const readSpy = vi.spyOn(cookies, 'read'); + + await sendRequest('/foo', { + xsrfCookieName: null, + }); + + expect(readSpy).not.toHaveBeenCalled(); + }); + + it('should not set xsrf header for cross origin', async () => { + setXsrfCookie('12345'); + + const request = await sendRequest('http://example.com/'); + + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toBeUndefined(); + }); + + it('should not set xsrf header for cross origin when using withCredentials', async () => { + setXsrfCookie('12345'); + + const request = await sendRequest('http://example.com/', { + withCredentials: true, + }); + + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toBeUndefined(); + }); + + describe('withXSRFToken option', () => { + it('should set xsrf header for cross origin when withXSRFToken = true', async () => { + const token = '12345'; + + setXsrfCookie(token); + + const request = await sendRequest('http://example.com/', { + withXSRFToken: true, + }); + + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toBe(token); + }); + + it('should not set xsrf header for the same origin when withXSRFToken = false', async () => { + const token = '12345'; + + setXsrfCookie(token); + + const request = await sendRequest('/foo', { + withXSRFToken: false, + }); + + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toBeUndefined(); + }); + + it('should support function resolver', async () => { + const token = '12345'; + + setXsrfCookie(token); + + const request = await sendRequest('/foo', { + withXSRFToken: (config) => config.userFlag === 'yes', + userFlag: 'yes', + }); + + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toBe(token); + }); + }); +}); diff --git a/tests/setup/server.js b/tests/setup/server.js index 90e32264..a07f9cfd 100644 --- a/tests/setup/server.js +++ b/tests/setup/server.js @@ -6,8 +6,6 @@ import { Throttle } from 'stream-throttle'; import formidable from 'formidable'; import selfsigned from 'selfsigned'; -export const LOCAL_SERVER_URL = 'http://localhost:4444'; - export const SERVER_HANDLER_STREAM_ECHO = (req, res) => req.pipe(res); export const setTimeoutAsync = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/tests/smoke/cjs/package-lock.json b/tests/smoke/cjs/package-lock.json new file mode 100644 index 00000000..dafab3ae --- /dev/null +++ b/tests/smoke/cjs/package-lock.json @@ -0,0 +1,1305 @@ +{ + "name": "@axios/cjs-smoke-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@axios/cjs-smoke-tests", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "axios": "file:../../../dist/node/axios.cjs" + }, + "devDependencies": { + "chai": "4.5.0", + "mocha": "9.2.2" + } + }, + "../..": { + "name": "axios", + "version": "1.13.6", + "extraneous": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + }, + "devDependencies": { + "@babel/core": "^7.28.6", + "@babel/preset-env": "^7.28.6", + "@commitlint/cli": "^20.3.1", + "@commitlint/config-conventional": "^20.3.1", + "@rollup/plugin-alias": "^5.1.1", + "@rollup/plugin-babel": "^6.1.0", + "@rollup/plugin-commonjs": "^15.1.0", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-multi-entry": "^4.1.0", + "@rollup/plugin-node-resolve": "^9.0.0", + "@vitest/browser": "^4.0.18", + "@vitest/browser-playwright": "^4.0.18", + "abortcontroller-polyfill": "^1.7.8", + "auto-changelog": "^2.5.0", + "body-parser": "^1.20.4", + "c8": "^10.1.3", + "chalk": "^5.6.2", + "coveralls": "^3.1.1", + "cross-env": "^7.0.3", + "dev-null": "^0.1.1", + "es6-promise": "^4.2.8", + "eslint": "^8.57.1", + "express": "^4.22.1", + "formdata-node": "^5.0.1", + "formidable": "^2.1.5", + "fs-extra": "^10.1.0", + "get-stream": "^3.0.0", + "gulp": "^4.0.2", + "handlebars": "^4.7.8", + "husky": "^8.0.3", + "istanbul-instrumenter-loader": "^3.0.1", + "jasmine-core": "^2.99.1", + "karma": "^6.4.4", + "karma-chrome-launcher": "^3.2.0", + "karma-firefox-launcher": "^2.1.3", + "karma-jasmine": "^1.1.2", + "karma-jasmine-ajax": "^0.1.13", + "karma-rollup-preprocessor": "^7.0.8", + "karma-safari-launcher": "^1.0.0", + "karma-sauce-launcher": "^4.3.6", + "karma-sinon": "^1.0.5", + "karma-sourcemap-loader": "^0.4.0", + "lint-staged": "^15.2.10", + "memoizee": "^0.4.17", + "minimist": "^1.2.8", + "mocha": "^10.8.2", + "multer": "^1.4.4", + "pacote": "^20.0.0", + "playwright": "^1.58.2", + "prettier": "^3.8.1", + "pretty-bytes": "^6.1.1", + "rollup": "^2.79.2", + "rollup-plugin-auto-external": "^2.0.0", + "rollup-plugin-bundle-size": "^1.0.3", + "rollup-plugin-terser": "^7.0.2", + "selfsigned": "^3.0.1", + "sinon": "^4.5.0", + "stream-throttle": "^0.1.3", + "string-replace-async": "^3.0.2", + "tar-stream": "^3.1.7", + "typescript": "^4.9.5", + "vitest": "^4.0.18" + } + }, + "../../..": { + "name": "axios", + "version": "1.13.6", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + }, + "devDependencies": { + "@babel/core": "^7.28.6", + "@babel/preset-env": "^7.28.6", + "@commitlint/cli": "^20.3.1", + "@commitlint/config-conventional": "^20.3.1", + "@rollup/plugin-alias": "^5.1.1", + "@rollup/plugin-babel": "^6.1.0", + "@rollup/plugin-commonjs": "^15.1.0", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-multi-entry": "^4.1.0", + "@rollup/plugin-node-resolve": "^9.0.0", + "@vitest/browser": "^4.0.18", + "@vitest/browser-playwright": "^4.0.18", + "abortcontroller-polyfill": "^1.7.8", + "auto-changelog": "^2.5.0", + "body-parser": "^1.20.4", + "c8": "^10.1.3", + "chalk": "^5.6.2", + "coveralls": "^3.1.1", + "cross-env": "^7.0.3", + "dev-null": "^0.1.1", + "es6-promise": "^4.2.8", + "eslint": "^8.57.1", + "express": "^4.22.1", + "formdata-node": "^5.0.1", + "formidable": "^2.1.5", + "fs-extra": "^10.1.0", + "get-stream": "^3.0.0", + "gulp": "^4.0.2", + "handlebars": "^4.7.8", + "husky": "^8.0.3", + "istanbul-instrumenter-loader": "^3.0.1", + "jasmine-core": "^2.99.1", + "karma": "^6.4.4", + "karma-chrome-launcher": "^3.2.0", + "karma-firefox-launcher": "^2.1.3", + "karma-jasmine": "^1.1.2", + "karma-jasmine-ajax": "^0.1.13", + "karma-rollup-preprocessor": "^7.0.8", + "karma-safari-launcher": "^1.0.0", + "karma-sauce-launcher": "^4.3.6", + "karma-sinon": "^1.0.5", + "karma-sourcemap-loader": "^0.4.0", + "lint-staged": "^15.2.10", + "memoizee": "^0.4.17", + "minimist": "^1.2.8", + "mocha": "^10.8.2", + "multer": "^1.4.4", + "pacote": "^20.0.0", + "playwright": "^1.58.2", + "prettier": "^3.8.1", + "pretty-bytes": "^6.1.1", + "rollup": "^2.79.2", + "rollup-plugin-auto-external": "^2.0.0", + "rollup-plugin-bundle-size": "^1.0.3", + "rollup-plugin-terser": "^7.0.2", + "selfsigned": "^3.0.1", + "sinon": "^4.5.0", + "stream-throttle": "^0.1.3", + "string-replace-async": "^3.0.2", + "tar-stream": "^3.1.7", + "typescript": "^4.9.5", + "vitest": "^4.0.18" + } + }, + "../../../dist": {}, + "../../../dist/node/axios.cjs": {}, + "../../dist": { + "extraneous": true + }, + "../../dist/node/axios.cjs": { + "extraneous": true + }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/axios": { + "resolved": "../../../dist/node/axios.cjs", + "link": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.x" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "4.2.1", + "ms": "2.1.3", + "nanoid": "3.3.1", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/nanoid": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "dev": true, + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tests/smoke/cjs/package.json b/tests/smoke/cjs/package.json new file mode 100644 index 00000000..d738538c --- /dev/null +++ b/tests/smoke/cjs/package.json @@ -0,0 +1,19 @@ +{ + "name": "@axios/cjs-smoke-tests", + "version": "1.0.0", + "description": "CJS smoke tests for axios", + "private": true, + "scripts": { + "test:smoke:cjs:mocha": "mocha \"tests/**/*.smoke.test.cjs\"" + }, + "keywords": [], + "author": "axios team", + "license": "MIT", + "dependencies": { + "axios": "file:../../../" + }, + "devDependencies": { + "mocha": "9.2.2", + "chai": "4.5.0" + } +} \ No newline at end of file diff --git a/tests/smoke/cjs/tests/auth.smoke.test.cjs b/tests/smoke/cjs/tests/auth.smoke.test.cjs new file mode 100644 index 00000000..ad63a4d9 --- /dev/null +++ b/tests/smoke/cjs/tests/auth.smoke.test.cjs @@ -0,0 +1,137 @@ +const http = require('http'); +const axios = require('axios'); +const { describe, it, afterEach } = require('mocha'); +const { expect } = require('chai'); + +const startServer = (handler) => { + return new Promise((resolve) => { + const server = http.createServer(handler); + + server.listen(0, '127.0.0.1', () => { + resolve(server); + }); + }); +}; + +const stopServer = (server) => { + if (!server || !server.listening) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); +}; + +describe('auth compat (dist export only)', () => { + let server; + + afterEach(async () => { + await stopServer(server); + server = undefined; + }); + + const requestWithConfig = async (config) => { + server = await startServer((req, res) => { + res.setHeader('Content-Type', 'text/plain'); + res.end(req.headers.authorization || ''); + }); + + const { port } = server.address(); + + return axios.get( + `http://127.0.0.1:${port}/`, + Object.assign( + { + proxy: false, + }, + config || {} + ) + ); + }; + + it('sets Basic Authorization header from auth credentials', async () => { + const response = await requestWithConfig({ + auth: { + username: 'janedoe', + password: 's00pers3cret', + }, + }); + + const expected = `Basic ${Buffer.from('janedoe:s00pers3cret', 'utf8').toString('base64')}`; + + expect(response.data).to.equal(expected); + }); + + it('supports auth without password', async () => { + const response = await requestWithConfig({ + auth: { + username: 'Aladdin', + }, + }); + + const expected = `Basic ${Buffer.from('Aladdin:', 'utf8').toString('base64')}`; + + expect(response.data).to.equal(expected); + }); + + it('overwrites an existing Authorization header when auth is provided', async () => { + const response = await requestWithConfig({ + headers: { + Authorization: 'Bearer token-123', + }, + auth: { + username: 'foo', + password: 'bar', + }, + }); + + const expected = `Basic ${Buffer.from('foo:bar', 'utf8').toString('base64')}`; + + expect(response.data).to.equal(expected); + }); + + it('uses URL credentials when auth config is not provided (node adapter behavior)', async () => { + server = await startServer((req, res) => { + res.setHeader('Content-Type', 'text/plain'); + res.end(req.headers.authorization || ''); + }); + + const { port } = server.address(); + + const response = await axios.get(`http://urluser:urlpass@127.0.0.1:${port}/`, { + proxy: false, + }); + + const expected = `Basic ${Buffer.from('urluser:urlpass', 'utf8').toString('base64')}`; + + expect(response.data).to.equal(expected); + }); + + it('prefers auth config over URL credentials', async () => { + server = await startServer((req, res) => { + res.setHeader('Content-Type', 'text/plain'); + res.end(req.headers.authorization || ''); + }); + + const { port } = server.address(); + + const response = await axios.get(`http://urluser:urlpass@127.0.0.1:${port}/`, { + proxy: false, + auth: { + username: 'configuser', + password: 'configpass', + }, + }); + + const expected = `Basic ${Buffer.from('configuser:configpass', 'utf8').toString('base64')}`; + + expect(response.data).to.equal(expected); + }); +}); diff --git a/tests/smoke/cjs/tests/basic.smoke.test.cjs b/tests/smoke/cjs/tests/basic.smoke.test.cjs new file mode 100644 index 00000000..dc63a2cc --- /dev/null +++ b/tests/smoke/cjs/tests/basic.smoke.test.cjs @@ -0,0 +1,145 @@ +const { EventEmitter } = require('events'); +const { PassThrough } = require('stream'); +const axios = require('axios'); +const { describe, it } = require('mocha'); +const { expect } = require('chai'); + +const createTransportCapture = () => { + let capturedOptions; + + const transport = { + request(options, onResponse) { + capturedOptions = options; + + const req = new EventEmitter(); + req.destroyed = false; + req.setTimeout = () => {}; + req.write = () => true; + req.end = () => { + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = { 'content-type': 'application/json' }; + res.req = req; + onResponse(res); + res.end('{"ok":true}'); + }; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + + return req; + }, + }; + + return { + transport, + getCapturedOptions: () => capturedOptions, + }; +}; + +const runRequest = async (run) => { + const { transport, getCapturedOptions } = createTransportCapture(); + await run(transport); + + return getCapturedOptions(); +}; + +describe('basic compat (dist export only)', () => { + it('supports the simplest axios(url) request pattern', async () => { + const options = await runRequest((transport) => + axios('http://example.com/users', { + transport, + proxy: false, + }) + ); + + expect(options.method).to.equal('GET'); + expect(options.path).to.equal('/users'); + }); + + it('supports get()', async () => { + const options = await runRequest((transport) => + axios.get('http://example.com/items?limit=10', { transport, proxy: false }) + ); + + expect(options.method).to.equal('GET'); + expect(options.path).to.equal('/items?limit=10'); + }); + + it('supports delete()', async () => { + const options = await runRequest((transport) => + axios.delete('http://example.com/items/1', { transport, proxy: false }) + ); + + expect(options.method).to.equal('DELETE'); + expect(options.path).to.equal('/items/1'); + }); + + it('supports head()', async () => { + const options = await runRequest((transport) => + axios.head('http://example.com/health', { transport, proxy: false }) + ); + + expect(options.method).to.equal('HEAD'); + expect(options.path).to.equal('/health'); + }); + + it('supports options()', async () => { + const options = await runRequest((transport) => + axios.options('http://example.com/items', { transport, proxy: false }) + ); + + expect(options.method).to.equal('OPTIONS'); + expect(options.path).to.equal('/items'); + }); + + it('supports post()', async () => { + const options = await runRequest((transport) => + axios.post( + 'http://example.com/items', + { name: 'widget' }, + { + transport, + proxy: false, + } + ) + ); + + expect(options.method).to.equal('POST'); + expect(options.path).to.equal('/items'); + }); + + it('supports put()', async () => { + const options = await runRequest((transport) => + axios.put( + 'http://example.com/items/1', + { name: 'updated-widget' }, + { + transport, + proxy: false, + } + ) + ); + + expect(options.method).to.equal('PUT'); + expect(options.path).to.equal('/items/1'); + }); + + it('supports patch()', async () => { + const options = await runRequest((transport) => + axios.patch( + 'http://example.com/items/1', + { status: 'active' }, + { + transport, + proxy: false, + } + ) + ); + + expect(options.method).to.equal('PATCH'); + expect(options.path).to.equal('/items/1'); + }); +}); diff --git a/tests/smoke/cjs/tests/cancel.smoke.test.cjs b/tests/smoke/cjs/tests/cancel.smoke.test.cjs new file mode 100644 index 00000000..58b2ca26 --- /dev/null +++ b/tests/smoke/cjs/tests/cancel.smoke.test.cjs @@ -0,0 +1,117 @@ +const { EventEmitter } = require('events'); +const axios = require('axios'); +const { describe, it } = require('mocha'); +const { expect } = require('chai'); + +const NODE_VERSION = parseInt(process.versions.node.split('.')[0]); +const itWithAbortController = NODE_VERSION < 16 ? it.skip : it; + +const createPendingTransport = () => { + let requestCount = 0; + + const transport = { + request() { + requestCount += 1; + + const req = new EventEmitter(); + req.destroyed = false; + req.setTimeout = () => {}; + req.write = () => true; + req.end = () => {}; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + + return req; + }, + }; + + return { + transport, + getRequestCount: () => requestCount, + }; +}; + +describe('cancel compat (dist export only)', () => { + itWithAbortController( + 'supports cancellation with AbortController (pre-aborted signal)', + async () => { + const { transport, getRequestCount } = createPendingTransport(); + const controller = new AbortController(); + controller.abort(); + + try { + const request = axios.get('http://example.com/resource', { + signal: controller.signal, + transport, + proxy: false, + }); + + controller.abort(); + await request; + } catch (error) { + expect(error).to.have.property('code', 'ERR_CANCELED'); + } + + expect(getRequestCount()).to.equal(0); + } + ); + + itWithAbortController('supports cancellation with AbortController (in-flight)', async () => { + const { transport, getRequestCount } = createPendingTransport(); + const controller = new AbortController(); + + try { + const request = axios.get('http://example.com/resource', { + signal: controller.signal, + transport, + proxy: false, + }); + + controller.abort(); + await request; + } catch (error) { + expect(error).to.have.property('code', 'ERR_CANCELED'); + } + + expect(getRequestCount()).to.equal(1); + }); + + it('supports cancellation with CancelToken (pre-canceled token)', async () => { + const { transport, getRequestCount } = createPendingTransport(); + const source = axios.CancelToken.source(); + source.cancel('Operation canceled by the user.'); + + const error = await axios + .get('http://example.com/resource', { + cancelToken: source.token, + transport, + proxy: false, + }) + .catch((err) => err); + + expect(axios.isCancel(error)).to.be.true; + expect(error.code).to.equal('ERR_CANCELED'); + expect(getRequestCount()).to.equal(0); + }); + + it('supports cancellation with CancelToken (in-flight)', async () => { + const { transport, getRequestCount } = createPendingTransport(); + const source = axios.CancelToken.source(); + + const request = axios.get('http://example.com/resource', { + cancelToken: source.token, + transport, + proxy: false, + }); + + source.cancel('Operation canceled by the user.'); + + const error = await request.catch((err) => err); + + expect(axios.isCancel(error)).to.be.true; + expect(error.code).to.equal('ERR_CANCELED'); + expect(getRequestCount()).to.equal(1); + }); +}); diff --git a/tests/smoke/cjs/tests/error.smoke.test.cjs b/tests/smoke/cjs/tests/error.smoke.test.cjs new file mode 100644 index 00000000..b7b31cce --- /dev/null +++ b/tests/smoke/cjs/tests/error.smoke.test.cjs @@ -0,0 +1,137 @@ +const { EventEmitter } = require('events'); +const { PassThrough } = require('stream'); +const axios = require('axios'); +const { describe, it } = require('mocha'); +const { expect } = require('chai'); + +const createTransport = (config) => { + const opts = config || {}; + + return { + request(options, onResponse) { + const req = new EventEmitter(); + req.destroyed = false; + req.write = () => true; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + + req.setTimeout = (_ms, cb) => { + if (opts.timeout) { + req._timeoutCallback = cb; + } + }; + + req.end = () => { + if (opts.error) { + req.emit('error', opts.error); + return; + } + + if (opts.timeout && req._timeoutCallback) { + req._timeoutCallback(); + return; + } + + const res = new PassThrough(); + res.statusCode = + opts.response && opts.response.statusCode !== undefined ? opts.response.statusCode : 200; + res.statusMessage = + opts.response && opts.response.statusMessage ? opts.response.statusMessage : 'OK'; + res.headers = + opts.response && opts.response.headers + ? opts.response.headers + : { 'content-type': 'application/json' }; + res.req = req; + + onResponse(res); + res.end(opts.response && opts.response.body ? opts.response.body : '{"ok":true}'); + }; + + return req; + }, + }; +}; + +describe('error compat (dist export only)', () => { + it('rejects with AxiosError for non-2xx responses by default', async () => { + const err = await axios + .get('http://example.com/fail', { + proxy: false, + transport: createTransport({ + response: { + statusCode: 500, + statusMessage: 'Internal Server Error', + body: '{"error":"boom"}', + }, + }), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).to.be.true; + expect(err.response.status).to.equal(500); + expect(err.message).to.include('500'); + }); + + it('resolves when validateStatus allows non-2xx responses', async () => { + const response = await axios.get('http://example.com/allowed', { + proxy: false, + validateStatus: () => true, + transport: createTransport({ + response: { + statusCode: 500, + statusMessage: 'Internal Server Error', + body: '{"ok":false}', + }, + }), + }); + + expect(response.status).to.equal(500); + expect(response.data).to.deep.equal({ ok: false }); + }); + + it('wraps transport errors as AxiosError', async () => { + const err = await axios + .get('http://example.com/network', { + proxy: false, + transport: createTransport({ + error: new Error('socket hang up'), + }), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).to.be.true; + expect(err.message).to.include('socket hang up'); + expect(err.toJSON).to.be.a('function'); + }); + + it('rejects with ECONNABORTED on timeout', async () => { + const err = await axios + .get('http://example.com/timeout', { + proxy: false, + timeout: 10, + transport: createTransport({ timeout: true }), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).to.be.true; + expect(err.code).to.equal('ECONNABORTED'); + expect(err.message).to.equal('timeout of 10ms exceeded'); + }); + + it('uses timeoutErrorMessage when provided', async () => { + const err = await axios + .get('http://example.com/timeout', { + proxy: false, + timeout: 25, + timeoutErrorMessage: 'custom timeout message', + transport: createTransport({ timeout: true }), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).to.be.true; + expect(err.code).to.equal('ECONNABORTED'); + expect(err.message).to.equal('custom timeout message'); + }); +}); diff --git a/tests/smoke/cjs/tests/fetch.smoke.test.cjs b/tests/smoke/cjs/tests/fetch.smoke.test.cjs new file mode 100644 index 00000000..a5fd0fea --- /dev/null +++ b/tests/smoke/cjs/tests/fetch.smoke.test.cjs @@ -0,0 +1,145 @@ +const axios = require('axios'); +const { it, describe } = require('mocha'); +const { expect } = require('chai'); + +const NODE_VERSION = parseInt(process.versions.node.split('.')[0]); +const describeWithFetch = NODE_VERSION < 18 ? describe.skip : describe; + +const createFetchMock = (responseFactory) => { + const calls = []; + + const mockFetch = async (input, init) => { + calls.push({ input, init: init || {} }); + + if (responseFactory) { + return responseFactory(input, init || {}); + } + + return new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }; + + return { + mockFetch, + getCalls: () => calls, + }; +}; + +describeWithFetch('fetch compat (dist export only)', () => { + it('uses fetch adapter and resolves JSON response', async () => { + const { mockFetch, getCalls } = createFetchMock(); + + const response = await axios.get('https://example.com/users', { + adapter: 'fetch', + env: { + fetch: mockFetch, + Request, + Response, + }, + }); + + expect(response.data).to.deep.equal({ ok: true }); + expect(response.status).to.equal(200); + expect(getCalls()).to.have.lengthOf(1); + }); + + it('sends method, headers and body for post requests', async () => { + const { mockFetch, getCalls } = createFetchMock(async (input, init) => { + const requestInit = init || {}; + const isRequest = input && typeof input !== 'string'; + const method = isRequest ? input.method : requestInit.method; + + const body = + isRequest && typeof input.clone === 'function' + ? await input.clone().text() + : requestInit.body; + + let contentType; + if (isRequest && input.headers) { + contentType = input.headers.get('content-type'); + } else if (requestInit.headers) { + contentType = requestInit.headers['Content-Type'] || requestInit.headers['content-type']; + } + + return new Response( + JSON.stringify({ + url: typeof input === 'string' ? input : input.url, + method, + contentType, + body, + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + } + ); + }); + + const response = await axios.post( + 'https://example.com/items', + { name: 'widget' }, + { + adapter: 'fetch', + env: { + fetch: mockFetch, + Request, + Response, + }, + } + ); + + expect(getCalls()).to.have.lengthOf(1); + expect(response.data.url).to.equal('https://example.com/items'); + expect(response.data.method).to.equal('POST'); + expect(response.data.contentType).to.include('application/json'); + expect(response.data.body).to.equal(JSON.stringify({ name: 'widget' })); + }); + + it('rejects non-2xx fetch responses by default', async () => { + const { mockFetch } = createFetchMock( + () => + new Response(JSON.stringify({ error: 'boom' }), { + status: 500, + statusText: 'Internal Server Error', + headers: { 'Content-Type': 'application/json' }, + }) + ); + + const err = await axios + .get('https://example.com/fail', { + adapter: 'fetch', + env: { + fetch: mockFetch, + Request, + Response, + }, + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).to.be.true; + expect(err.response.status).to.equal(500); + }); + + it('supports cancellation with AbortController in fetch mode', async () => { + const { mockFetch } = createFetchMock(); + const controller = new AbortController(); + controller.abort(); + + const err = await axios + .get('https://example.com/cancel', { + adapter: 'fetch', + signal: controller.signal, + env: { + fetch: mockFetch, + Request, + Response, + }, + }) + .catch((e) => e); + + expect(axios.isCancel(err)).to.be.true; + expect(err.code).to.equal('ERR_CANCELED'); + }); +}); diff --git a/tests/smoke/cjs/tests/files.smoke.test.cjs b/tests/smoke/cjs/tests/files.smoke.test.cjs new file mode 100644 index 00000000..5515bb2e --- /dev/null +++ b/tests/smoke/cjs/tests/files.smoke.test.cjs @@ -0,0 +1,115 @@ +const { PassThrough, Readable, Writable } = require('stream'); +const axios = require('axios'); +const { describe, it } = require('mocha'); +const { expect } = require('chai'); + +const createCaptureTransport = (buildResponse) => { + return { + request(options, onResponse) { + const chunks = []; + + const req = new Writable({ + write(chunk, _encoding, callback) { + chunks.push(Buffer.from(chunk)); + callback(); + }, + }); + + req.destroyed = false; + req.setTimeout = () => {}; + req.close = () => { + req.destroyed = true; + }; + + const originalDestroy = req.destroy.bind(req); + req.destroy = (...args) => { + req.destroyed = true; + return originalDestroy(...args); + }; + + const originalEnd = req.end.bind(req); + req.end = (...args) => { + originalEnd(...args); + + const body = Buffer.concat(chunks); + const response = buildResponse ? buildResponse(body, options) : {}; + + const res = new PassThrough(); + res.statusCode = response.statusCode !== undefined ? response.statusCode : 200; + res.statusMessage = response.statusMessage || 'OK'; + res.headers = response.headers || { 'content-type': 'application/json' }; + res.req = req; + + onResponse(res); + res.end(response.body || JSON.stringify({ size: body.length })); + }; + + req.on('error', () => {}); + return req; + }, + }; +}; + +describe('files compat (dist export only)', () => { + it('supports posting Buffer payloads', async () => { + const source = Buffer.from('binary-\x00-data', 'utf8'); + + const response = await axios.post('http://example.com/upload', source, { + proxy: false, + transport: createCaptureTransport((body) => ({ + body: JSON.stringify({ echoed: body.toString('base64') }), + })), + }); + + expect(response.data.echoed).to.equal(source.toString('base64')); + }); + + it('supports posting Uint8Array payloads', async () => { + const source = Uint8Array.from([1, 2, 3, 4, 255]); + + const response = await axios.post('http://example.com/upload', source, { + proxy: false, + transport: createCaptureTransport((body) => ({ + body: JSON.stringify({ echoed: Array.from(body.values()) }), + })), + }); + + expect(response.data.echoed).to.deep.equal([1, 2, 3, 4, 255]); + }); + + it('supports posting Readable stream payloads', async () => { + const streamData = ['hello ', 'stream ', 'world']; + const source = Readable.from(streamData); + + const response = await axios.post('http://example.com/upload', source, { + proxy: false, + headers: { 'Content-Type': 'application/octet-stream' }, + transport: createCaptureTransport((body, options) => ({ + body: JSON.stringify({ + text: body.toString('utf8'), + contentType: + options.headers && (options.headers['Content-Type'] || options.headers['content-type']), + }), + })), + }); + + expect(response.data.text).to.equal('hello stream world'); + expect(response.data.contentType).to.contain('application/octet-stream'); + }); + + it('supports binary downloads with responseType=arraybuffer', async () => { + const binary = Buffer.from([0xde, 0xad, 0xbe, 0xef]); + + const response = await axios.get('http://example.com/file.bin', { + proxy: false, + responseType: 'arraybuffer', + transport: createCaptureTransport(() => ({ + headers: { 'content-type': 'application/octet-stream' }, + body: binary, + })), + }); + + expect(Buffer.isBuffer(response.data)).to.equal(true); + expect(response.data.equals(binary)).to.equal(true); + }); +}); diff --git a/tests/smoke/cjs/tests/formData.smoke.test.cjs b/tests/smoke/cjs/tests/formData.smoke.test.cjs new file mode 100644 index 00000000..a93c3a7e --- /dev/null +++ b/tests/smoke/cjs/tests/formData.smoke.test.cjs @@ -0,0 +1,110 @@ +const { Writable, PassThrough } = require('stream'); +const axios = require('axios'); +const { describe, it } = require('mocha'); +const { expect } = require('chai'); + +const NODE_VERSION = parseInt(process.versions.node.split('.')[0]); +const describeWithFormData = NODE_VERSION < 18 ? describe.skip : describe; + +const createCaptureTransport = (buildResponse) => { + return { + request(options, onResponse) { + const chunks = []; + + const req = new Writable({ + write(chunk, _encoding, callback) { + chunks.push(Buffer.from(chunk)); + callback(); + }, + }); + + req.destroyed = false; + req.setTimeout = () => {}; + req.write = req.write.bind(req); + req.close = () => { + req.destroyed = true; + }; + + const originalDestroy = req.destroy.bind(req); + req.destroy = (...args) => { + req.destroyed = true; + return originalDestroy(...args); + }; + + const originalEnd = req.end.bind(req); + req.end = (...args) => { + originalEnd(...args); + + const body = Buffer.concat(chunks); + const response = buildResponse ? buildResponse(body, options) : {}; + + const res = new PassThrough(); + res.statusCode = response.statusCode !== undefined ? response.statusCode : 200; + res.statusMessage = response.statusMessage || 'OK'; + res.headers = response.headers || { 'content-type': 'application/json' }; + res.req = req; + + onResponse(res); + res.end(response.body || JSON.stringify({ ok: true })); + }; + + return req; + }, + }; +}; + +const bodyAsUtf8 = (value) => { + return Buffer.isBuffer(value) ? value.toString('utf8') : String(value); +}; + +describeWithFormData('formData compat (dist export only)', () => { + it('supports posting FormData instances', async () => { + const form = new FormData(); + form.append('username', 'janedoe'); + form.append('role', 'admin'); + + const response = await axios.post('http://example.com/form', form, { + proxy: false, + transport: createCaptureTransport((body, options) => ({ + body: JSON.stringify({ + contentType: + options.headers && (options.headers['Content-Type'] || options.headers['content-type']), + payload: bodyAsUtf8(body), + }), + })), + }); + + expect(response.data.contentType).to.contain('multipart/form-data'); + expect(response.data.payload).to.contain('name="username"'); + expect(response.data.payload).to.contain('janedoe'); + expect(response.data.payload).to.contain('name="role"'); + expect(response.data.payload).to.contain('admin'); + }); + + it('supports axios.postForm helper', async () => { + const response = await axios.postForm( + 'http://example.com/post-form', + { + project: 'axios', + mode: 'compat', + }, + { + proxy: false, + transport: createCaptureTransport((body, options) => ({ + body: JSON.stringify({ + contentType: + options.headers && + (options.headers['Content-Type'] || options.headers['content-type']), + payload: bodyAsUtf8(body), + }), + })), + } + ); + + expect(response.data.contentType).to.contain('multipart/form-data'); + expect(response.data.payload).to.contain('name="project"'); + expect(response.data.payload).to.contain('axios'); + expect(response.data.payload).to.contain('name="mode"'); + expect(response.data.payload).to.contain('compat'); + }); +}); diff --git a/tests/smoke/cjs/tests/headers.smoke.test.cjs b/tests/smoke/cjs/tests/headers.smoke.test.cjs new file mode 100644 index 00000000..bdf07278 --- /dev/null +++ b/tests/smoke/cjs/tests/headers.smoke.test.cjs @@ -0,0 +1,125 @@ +const { EventEmitter } = require('events'); +const { PassThrough } = require('stream'); +const axios = require('axios'); +const { describe, it } = require('mocha'); +const { expect } = require('chai'); + +const normalizeHeaders = (headers) => { + const result = {}; + + Object.entries(headers || {}).forEach(([key, value]) => { + result[key.toLowerCase()] = value; + }); + + return result; +}; + +const createTransportCapture = () => { + let capturedOptions; + + const transport = { + request(options, onResponse) { + capturedOptions = options; + + const req = new EventEmitter(); + req.destroyed = false; + req.setTimeout = () => {}; + req.write = () => true; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + req.end = () => { + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = { 'content-type': 'application/json' }; + res.req = req; + onResponse(res); + res.end('{"ok":true}'); + }; + + return req; + }, + }; + + return { + transport, + getCapturedOptions: () => capturedOptions, + }; +}; + +describe('headers compat (dist export only)', () => { + it('sends default Accept header', async () => { + const { transport, getCapturedOptions } = createTransportCapture(); + + await axios.get('http://example.com/default-headers', { + transport, + proxy: false, + }); + + const headers = normalizeHeaders(getCapturedOptions().headers); + expect(headers.accept).to.equal('application/json, text/plain, */*'); + }); + + it('supports custom headers', async () => { + const { transport, getCapturedOptions } = createTransportCapture(); + + await axios.get('http://example.com/custom-headers', { + transport, + proxy: false, + headers: { + 'X-Trace-Id': 'trace-123', + Authorization: 'Bearer token-abc', + }, + }); + + const headers = normalizeHeaders(getCapturedOptions().headers); + expect(headers['x-trace-id']).to.equal('trace-123'); + expect(headers.authorization).to.equal('Bearer token-abc'); + }); + + it('treats header names as case-insensitive when overriding', async () => { + const { transport, getCapturedOptions } = createTransportCapture(); + + await axios.get('http://example.com/case-insensitive', { + transport, + proxy: false, + headers: { + authorization: 'Bearer old-token', + AuThOrIzAtIoN: 'Bearer new-token', + }, + }); + + const headers = normalizeHeaders(getCapturedOptions().headers); + expect(headers.authorization).to.equal('Bearer new-token'); + }); + + it('sets content-type for json post payloads', async () => { + const { transport, getCapturedOptions } = createTransportCapture(); + + await axios.post( + 'http://example.com/post-json', + { name: 'widget' }, + { + transport, + proxy: false, + } + ); + + const headers = normalizeHeaders(getCapturedOptions().headers); + expect(headers['content-type']).to.contain('application/json'); + }); + + it('does not force content-type for get requests without body', async () => { + const { transport, getCapturedOptions } = createTransportCapture(); + + await axios.get('http://example.com/get-no-body', { + transport, + proxy: false, + }); + + const headers = normalizeHeaders(getCapturedOptions().headers); + expect(headers['content-type']).to.be.undefined; + }); +}); diff --git a/tests/smoke/cjs/tests/http2.smoke.test.cjs b/tests/smoke/cjs/tests/http2.smoke.test.cjs new file mode 100644 index 00000000..3b9a59b0 --- /dev/null +++ b/tests/smoke/cjs/tests/http2.smoke.test.cjs @@ -0,0 +1,86 @@ +const axios = require('axios'); +const { describe, it } = require('mocha'); +const { expect } = require('chai'); + +describe('http2 compat (dist export only)', () => { + it('keeps instance-level httpVersion and http2Options in request config', async () => { + const client = axios.create({ + baseURL: 'https://example.com', + httpVersion: 2, + http2Options: { + rejectUnauthorized: false, + }, + }); + + const response = await client.get('/resource', { + adapter: async (config) => ({ + data: { + httpVersion: config.httpVersion, + http2Options: config.http2Options, + }, + status: 200, + statusText: 'OK', + headers: {}, + config, + }), + }); + + expect(response.data.httpVersion).to.equal(2); + expect(response.data.http2Options).to.deep.equal({ + rejectUnauthorized: false, + }); + }); + + it('merges request http2Options with instance http2Options', async () => { + const client = axios.create({ + baseURL: 'https://example.com', + httpVersion: 2, + http2Options: { + rejectUnauthorized: false, + sessionTimeout: 1000, + }, + }); + + const response = await client.get('/resource', { + http2Options: { + sessionTimeout: 5000, + customFlag: true, + }, + adapter: async (config) => ({ + data: config.http2Options, + status: 200, + statusText: 'OK', + headers: {}, + config, + }), + }); + + expect(response.data).to.deep.equal({ + rejectUnauthorized: false, + sessionTimeout: 5000, + customFlag: true, + }); + }); + + it('allows request-level httpVersion override', async () => { + const client = axios.create({ + baseURL: 'https://example.com', + httpVersion: 2, + }); + + const response = await client.get('/resource', { + httpVersion: 1, + adapter: async (config) => ({ + data: { + httpVersion: config.httpVersion, + }, + status: 200, + statusText: 'OK', + headers: {}, + config, + }), + }); + + expect(response.data.httpVersion).to.equal(1); + }); +}); diff --git a/tests/smoke/cjs/tests/instance.smoke.test.cjs b/tests/smoke/cjs/tests/instance.smoke.test.cjs new file mode 100644 index 00000000..210f5bf6 --- /dev/null +++ b/tests/smoke/cjs/tests/instance.smoke.test.cjs @@ -0,0 +1,146 @@ +const { EventEmitter } = require('events'); +const { PassThrough } = require('stream'); +const axios = require('axios'); +const { describe, it } = require('mocha'); +const { expect } = require('chai'); + +const createTransportCapture = (responseBody) => { + const calls = []; + + const transport = { + request(options, onResponse) { + calls.push(options); + + const req = new EventEmitter(); + req.destroyed = false; + req.setTimeout = () => {}; + req.write = () => true; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + req.end = () => { + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = { 'content-type': 'application/json' }; + res.req = req; + onResponse(res); + res.end(responseBody || '{"ok":true}'); + }; + + return req; + }, + }; + + return { + transport, + getCalls: () => calls, + }; +}; + +describe('instance compat (dist export only)', () => { + it('creates isolated instances with separate defaults', async () => { + const { transport, getCalls } = createTransportCapture(); + + const clientA = axios.create({ + baseURL: 'http://example.com/api-a', + headers: { + 'X-App': 'A', + }, + }); + const clientB = axios.create({ + baseURL: 'http://example.com/api-b', + headers: { + 'X-App': 'B', + }, + }); + + await clientA.get('/users', { transport, proxy: false }); + await clientB.get('/users', { transport, proxy: false }); + + const [callA, callB] = getCalls(); + expect(callA.path).to.equal('/api-a/users'); + expect(callB.path).to.equal('/api-b/users'); + expect(callA.headers['X-App']).to.equal('A'); + expect(callB.headers['X-App']).to.equal('B'); + }); + + it('supports callable instance form instance(config)', async () => { + const { transport, getCalls } = createTransportCapture(); + const client = axios.create({ + baseURL: 'http://example.com', + }); + + await client({ + url: '/status', + method: 'get', + transport, + proxy: false, + }); + + expect(getCalls()).to.have.lengthOf(1); + expect(getCalls()[0].method).to.equal('GET'); + expect(getCalls()[0].path).to.equal('/status'); + }); + + it('applies instance request interceptors', async () => { + const { transport, getCalls } = createTransportCapture(); + const client = axios.create({ + baseURL: 'http://example.com', + }); + + client.interceptors.request.use((config) => { + config.headers = config.headers || {}; + config.headers['X-From-Interceptor'] = 'yes'; + return config; + }); + + await client.get('/intercepted', { transport, proxy: false }); + + expect(getCalls()).to.have.lengthOf(1); + expect(getCalls()[0].headers['X-From-Interceptor']).to.equal('yes'); + }); + + it('applies instance response interceptors', async () => { + const { transport } = createTransportCapture('{"name":"axios"}'); + const client = axios.create({ + baseURL: 'http://example.com', + }); + + client.interceptors.response.use((response) => { + response.data = Object.assign({}, response.data, { + intercepted: true, + }); + return response; + }); + + const response = await client.get('/response-interceptor', { + transport, + proxy: false, + }); + + expect(response.data).to.deep.equal({ + name: 'axios', + intercepted: true, + }); + }); + + it('builds URLs with getUri from instance defaults and request params', () => { + const client = axios.create({ + baseURL: 'http://example.com/api', + params: { + apiKey: 'abc', + }, + }); + + const uri = client.getUri({ + url: '/users', + params: { + page: 2, + }, + }); + + expect(uri).to.equal('http://example.com/api/users?apiKey=abc&page=2'); + }); +}); diff --git a/tests/smoke/cjs/tests/interceptors.smoke.test.cjs b/tests/smoke/cjs/tests/interceptors.smoke.test.cjs new file mode 100644 index 00000000..eb009e31 --- /dev/null +++ b/tests/smoke/cjs/tests/interceptors.smoke.test.cjs @@ -0,0 +1,149 @@ +const { EventEmitter } = require('events'); +const { PassThrough } = require('stream'); +const axios = require('axios'); +const { describe, it } = require('mocha'); +const { expect } = require('chai'); + +const createTransport = (responseBody) => { + const calls = []; + + const transport = { + request(options, onResponse) { + calls.push(options); + + const req = new EventEmitter(); + req.destroyed = false; + req.setTimeout = () => {}; + req.write = () => true; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + req.end = () => { + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = { 'content-type': 'application/json' }; + res.req = req; + onResponse(res); + res.end(responseBody || '{"value":"ok"}'); + }; + + return req; + }, + }; + + return { + transport, + getCalls: () => calls, + }; +}; + +describe('interceptors compat (dist export only)', () => { + it('applies request interceptors before dispatch', async () => { + const { transport, getCalls } = createTransport(); + const client = axios.create(); + + client.interceptors.request.use((config) => { + config.headers = config.headers || {}; + config.headers['X-One'] = '1'; + return config; + }); + + client.interceptors.request.use((config) => { + config.headers['X-Two'] = '2'; + return config; + }); + + await client.get('http://example.com/resource', { + transport, + proxy: false, + }); + + expect(getCalls()).to.have.lengthOf(1); + expect(getCalls()[0].headers['X-One']).to.equal('1'); + expect(getCalls()[0].headers['X-Two']).to.equal('2'); + }); + + it('applies response interceptors in registration order', async () => { + const { transport } = createTransport('{"n":1}'); + const client = axios.create(); + + client.interceptors.response.use((response) => { + response.data.n += 1; + return response; + }); + + client.interceptors.response.use((response) => { + response.data.n *= 10; + return response; + }); + + const response = await client.get('http://example.com/resource', { + transport, + proxy: false, + }); + + expect(response.data.n).to.equal(20); + }); + + it('supports ejecting request interceptors', async () => { + const { transport, getCalls } = createTransport(); + const client = axios.create(); + + const id = client.interceptors.request.use((config) => { + config.headers = config.headers || {}; + config.headers['X-Ejected'] = 'yes'; + return config; + }); + + client.interceptors.request.eject(id); + + await client.get('http://example.com/resource', { + transport, + proxy: false, + }); + + expect(getCalls()).to.have.lengthOf(1); + expect(getCalls()[0].headers['X-Ejected']).to.be.undefined; + }); + + it('supports async request interceptors', async () => { + const { transport, getCalls } = createTransport(); + const client = axios.create(); + + client.interceptors.request.use(async (config) => { + await Promise.resolve(); + config.headers = config.headers || {}; + config.headers['X-Async'] = 'true'; + return config; + }); + + await client.get('http://example.com/resource', { + transport, + proxy: false, + }); + + expect(getCalls()[0].headers['X-Async']).to.equal('true'); + }); + + it('propagates errors thrown by request interceptors', async () => { + const { transport, getCalls } = createTransport(); + const client = axios.create(); + + client.interceptors.request.use(() => { + throw new Error('blocked-by-interceptor'); + }); + + const err = await client + .get('http://example.com/resource', { + transport, + proxy: false, + }) + .catch((e) => e); + + expect(err).to.be.instanceOf(Error); + expect(err.message).to.contain('blocked-by-interceptor'); + expect(getCalls()).to.have.lengthOf(0); + }); +}); diff --git a/tests/smoke/cjs/tests/progress.smoke.test.cjs b/tests/smoke/cjs/tests/progress.smoke.test.cjs new file mode 100644 index 00000000..12771a57 --- /dev/null +++ b/tests/smoke/cjs/tests/progress.smoke.test.cjs @@ -0,0 +1,113 @@ +const { Readable, Writable, PassThrough } = require('stream'); +const axios = require('axios'); +const { describe, it } = require('mocha'); +const { expect } = require('chai'); + +const createProgressTransport = (config) => { + const opts = config || {}; + const responseChunks = opts.responseChunks || ['ok']; + const responseHeaders = opts.responseHeaders || {}; + + return { + request(_options, onResponse) { + const req = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + }); + + req.destroyed = false; + req.setTimeout = () => {}; + req.close = () => { + req.destroyed = true; + }; + + const originalDestroy = req.destroy.bind(req); + req.destroy = (...args) => { + req.destroyed = true; + return originalDestroy(...args); + }; + + const originalEnd = req.end.bind(req); + req.end = (...args) => { + originalEnd(...args); + + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = Object.assign( + { + 'content-type': 'text/plain', + }, + responseHeaders + ); + res.req = req; + + onResponse(res); + + responseChunks.forEach((chunk) => { + res.write(chunk); + }); + res.end(); + }; + + return req; + }, + }; +}; + +describe('progress compat (dist export only)', () => { + it('emits upload progress events for stream payloads', async () => { + const samples = []; + const payload = ['abc', 'def', 'ghi']; + const total = payload.join('').length; + + await axios.post('http://example.com/upload', Readable.from(payload), { + proxy: false, + headers: { + 'Content-Length': String(total), + }, + onUploadProgress: ({ loaded, total: reportedTotal, upload }) => { + samples.push({ loaded, total: reportedTotal, upload }); + }, + transport: createProgressTransport({ + responseChunks: ['uploaded'], + }), + }); + + expect(samples.length).to.be.greaterThan(0); + expect(samples[samples.length - 1]).to.deep.include({ + loaded: total, + total, + upload: true, + }); + }); + + it('emits download progress events', async () => { + const samples = []; + const chunks = ['ab', 'cd', 'ef']; + const total = chunks.join('').length; + + const response = await axios.get('http://example.com/download', { + proxy: false, + responseType: 'text', + onDownloadProgress: ({ loaded, total: reportedTotal, download }) => { + samples.push({ loaded, total: reportedTotal, download }); + }, + transport: createProgressTransport({ + responseChunks: chunks, + responseHeaders: { + 'content-length': String(total), + }, + }), + }); + + expect(response.data).to.equal('abcdef'); + expect(samples.length).to.be.greaterThan(0); + expect(samples[samples.length - 1]).to.deep.include({ + loaded: total, + total, + download: true, + }); + }); +}); diff --git a/tests/smoke/cjs/tests/rateLimit.smoke.test.cjs b/tests/smoke/cjs/tests/rateLimit.smoke.test.cjs new file mode 100644 index 00000000..71608bde --- /dev/null +++ b/tests/smoke/cjs/tests/rateLimit.smoke.test.cjs @@ -0,0 +1,105 @@ +const { EventEmitter } = require('events'); +const { PassThrough } = require('stream'); +const axios = require('axios'); +const { describe, it } = require('mocha'); +const { expect } = require('chai'); + +const createTransportCapture = () => { + let capturedOptions; + + const transport = { + request(options, onResponse) { + capturedOptions = options; + + const req = new EventEmitter(); + req.destroyed = false; + req.setTimeout = () => {}; + req.write = () => true; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + req.end = () => { + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = { 'content-type': 'application/json' }; + res.req = req; + onResponse(res); + res.end('{"ok":true}'); + }; + + return req; + }, + }; + + return { + transport, + getCapturedOptions: () => capturedOptions, + }; +}; + +describe('rateLimit compat (dist export only)', () => { + it('accepts numeric maxRate config', async () => { + const response = await axios.get('http://example.com/rate', { + maxRate: 1024, + adapter: async (config) => ({ + data: { maxRate: config.maxRate }, + status: 200, + statusText: 'OK', + headers: {}, + config, + }), + }); + + expect(response.data.maxRate).to.equal(1024); + }); + + it('accepts tuple maxRate config [upload, download]', async () => { + const response = await axios.get('http://example.com/rate', { + maxRate: [2048, 4096], + adapter: async (config) => ({ + data: { maxRate: config.maxRate }, + status: 200, + statusText: 'OK', + headers: {}, + config, + }), + }); + + expect(response.data.maxRate).to.deep.equal([2048, 4096]); + }); + + it('merges instance and request maxRate values', async () => { + const client = axios.create({ + maxRate: [1000, 2000], + }); + + const response = await client.get('http://example.com/rate', { + maxRate: [3000, 4000], + adapter: async (config) => ({ + data: { maxRate: config.maxRate }, + status: 200, + statusText: 'OK', + headers: {}, + config, + }), + }); + + expect(response.data.maxRate).to.deep.equal([3000, 4000]); + }); + + it('supports maxRate in node transport flow without errors', async () => { + const { transport, getCapturedOptions } = createTransportCapture(); + + const response = await axios.get('http://example.com/rate', { + proxy: false, + maxRate: [1500, 2500], + transport, + }); + + expect(response.status).to.equal(200); + expect(getCapturedOptions().method).to.equal('GET'); + expect(getCapturedOptions().path).to.equal('/rate'); + }); +}); diff --git a/tests/smoke/cjs/tests/timeout.smoke.test.cjs b/tests/smoke/cjs/tests/timeout.smoke.test.cjs new file mode 100644 index 00000000..56acec83 --- /dev/null +++ b/tests/smoke/cjs/tests/timeout.smoke.test.cjs @@ -0,0 +1,116 @@ +const { EventEmitter } = require('events'); +const { PassThrough } = require('stream'); +const axios = require('axios'); +const { describe, it } = require('mocha'); +const { expect } = require('chai'); + +const createTransport = (config) => { + const opts = config || {}; + + return { + request(_options, onResponse) { + const req = new EventEmitter(); + req.destroyed = false; + req._timeoutCallback = null; + + req.setTimeout = (_ms, callback) => { + req._timeoutCallback = callback; + }; + + req.write = () => true; + + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + + req.end = () => { + if (opts.triggerTimeout && req._timeoutCallback) { + req._timeoutCallback(); + return; + } + + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = { 'content-type': 'application/json' }; + res.req = req; + + onResponse(res); + res.end(opts.body || '{"ok":true}'); + }; + + return req; + }, + }; +}; + +describe('timeout compat (dist export only)', () => { + it('rejects with ECONNABORTED on timeout', async () => { + const err = await axios + .get('http://example.com/timeout', { + proxy: false, + timeout: 25, + transport: createTransport({ triggerTimeout: true }), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).to.equal(true); + expect(err.code).to.equal('ECONNABORTED'); + expect(err.message).to.equal('timeout of 25ms exceeded'); + }); + + it('uses timeoutErrorMessage when provided', async () => { + const err = await axios + .get('http://example.com/timeout', { + proxy: false, + timeout: 25, + timeoutErrorMessage: 'custom timeout', + transport: createTransport({ triggerTimeout: true }), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).to.equal(true); + expect(err.code).to.equal('ECONNABORTED'); + expect(err.message).to.equal('custom timeout'); + }); + + it('accepts timeout as a numeric string', async () => { + const err = await axios + .get('http://example.com/timeout', { + proxy: false, + timeout: '30', + transport: createTransport({ triggerTimeout: true }), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).to.equal(true); + expect(err.code).to.equal('ECONNABORTED'); + expect(err.message).to.equal('timeout of 30ms exceeded'); + }); + + it('rejects with ERR_BAD_OPTION_VALUE when timeout is not parsable', async () => { + const err = await axios + .get('http://example.com/timeout', { + proxy: false, + timeout: { invalid: true }, + transport: createTransport(), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).to.equal(true); + expect(err.code).to.equal('ERR_BAD_OPTION_VALUE'); + expect(err.message).to.equal('error trying to parse `config.timeout` to int'); + }); + + it('does not time out when timeout is 0', async () => { + const response = await axios.get('http://example.com/no-timeout', { + proxy: false, + timeout: 0, + transport: createTransport({ body: '{"ok":true}' }), + }); + + expect(response.status).to.equal(200); + expect(response.data).to.deep.equal({ ok: true }); + }); +}); diff --git a/tests/smoke/cjs/tests/urlencode.smoke.test.cjs b/tests/smoke/cjs/tests/urlencode.smoke.test.cjs new file mode 100644 index 00000000..1cd9403d --- /dev/null +++ b/tests/smoke/cjs/tests/urlencode.smoke.test.cjs @@ -0,0 +1,146 @@ +const { EventEmitter } = require('events'); +const { PassThrough } = require('stream'); +const axios = require('axios'); +const { describe, it } = require('mocha'); +const { expect } = require('chai'); + +const createEchoTransport = () => { + let capturedOptions; + + const transport = { + request(options, onResponse) { + capturedOptions = options; + const chunks = []; + + const req = new EventEmitter(); + req.destroyed = false; + req.setTimeout = () => {}; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + req.write = (chunk) => { + chunks.push(Buffer.from(chunk)); + return true; + }; + req.end = (chunk) => { + if (chunk) { + chunks.push(Buffer.from(chunk)); + } + + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = { 'content-type': 'application/json' }; + res.req = req; + onResponse(res); + res.end( + JSON.stringify({ + path: options.path, + body: Buffer.concat(chunks).toString('utf8'), + contentType: + options.headers && + (options.headers['Content-Type'] || options.headers['content-type']), + }) + ); + }; + + return req; + }, + }; + + return { + transport, + getCapturedOptions: () => capturedOptions, + }; +}; + +describe('urlencode compat (dist export only)', () => { + it('serializes params into request URL', async () => { + const { transport } = createEchoTransport(); + + const response = await axios.get('http://example.com/search', { + proxy: false, + transport, + params: { + q: 'axios docs', + page: 2, + }, + }); + + expect(response.data.path).to.equal('/search?q=axios+docs&page=2'); + }); + + it('supports custom paramsSerializer function', async () => { + const { transport } = createEchoTransport(); + + const response = await axios.get('http://example.com/search', { + proxy: false, + transport, + params: { q: 'ignored' }, + paramsSerializer: () => 'fixed=1', + }); + + expect(response.data.path).to.equal('/search?fixed=1'); + }); + + it('supports URLSearchParams payloads', async () => { + const { transport } = createEchoTransport(); + const payload = new URLSearchParams(); + payload.append('name', 'axios'); + payload.append('mode', 'compat'); + + const response = await axios.post('http://example.com/form', payload, { + proxy: false, + transport, + }); + + expect(response.data.body).to.equal('name=axios&mode=compat'); + expect(response.data.contentType).to.contain('application/x-www-form-urlencoded'); + }); + + it('serializes object payload when content-type is application/x-www-form-urlencoded', async () => { + const { transport } = createEchoTransport(); + + const response = await axios.post( + 'http://example.com/form', + { + name: 'axios', + mode: 'compat', + }, + { + proxy: false, + transport, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + } + ); + + expect(response.data.body).to.equal('name=axios&mode=compat'); + expect(response.data.contentType).to.contain('application/x-www-form-urlencoded'); + }); + + it('respects formSerializer options for index formatting', async () => { + const { transport } = createEchoTransport(); + + const response = await axios.post( + 'http://example.com/form', + { + arr: ['1', '2'], + }, + { + proxy: false, + transport, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + formSerializer: { + indexes: true, + }, + } + ); + + expect(response.data.body).to.equal('arr%5B0%5D=1&arr%5B1%5D=2'); + }); +}); diff --git a/tests/smoke/esm/package-lock.json b/tests/smoke/esm/package-lock.json new file mode 100644 index 00000000..7e4e98f2 --- /dev/null +++ b/tests/smoke/esm/package-lock.json @@ -0,0 +1,1549 @@ +{ + "name": "@axios/esm-smoke-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@axios/esm-smoke-tests", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "axios": "file:../../../" + }, + "devDependencies": { + "vitest": "^4.0.18" + } + }, + "../../..": { + "name": "axios", + "version": "1.13.6", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + }, + "devDependencies": { + "@babel/core": "^7.28.6", + "@babel/preset-env": "^7.28.6", + "@commitlint/cli": "^20.3.1", + "@commitlint/config-conventional": "^20.3.1", + "@rollup/plugin-alias": "^5.1.1", + "@rollup/plugin-babel": "^6.1.0", + "@rollup/plugin-commonjs": "^15.1.0", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-multi-entry": "^4.1.0", + "@rollup/plugin-node-resolve": "^9.0.0", + "@vitest/browser": "^4.0.18", + "@vitest/browser-playwright": "^4.0.18", + "abortcontroller-polyfill": "^1.7.8", + "auto-changelog": "^2.5.0", + "body-parser": "^1.20.4", + "c8": "^10.1.3", + "chalk": "^5.6.2", + "coveralls": "^3.1.1", + "cross-env": "^7.0.3", + "dev-null": "^0.1.1", + "es6-promise": "^4.2.8", + "eslint": "^8.57.1", + "express": "^4.22.1", + "formdata-node": "^5.0.1", + "formidable": "^2.1.5", + "fs-extra": "^10.1.0", + "get-stream": "^3.0.0", + "gulp": "^4.0.2", + "handlebars": "^4.7.8", + "husky": "^8.0.3", + "istanbul-instrumenter-loader": "^3.0.1", + "jasmine-core": "^2.99.1", + "karma": "^6.4.4", + "karma-chrome-launcher": "^3.2.0", + "karma-firefox-launcher": "^2.1.3", + "karma-jasmine": "^1.1.2", + "karma-jasmine-ajax": "^0.1.13", + "karma-rollup-preprocessor": "^7.0.8", + "karma-safari-launcher": "^1.0.0", + "karma-sauce-launcher": "^4.3.6", + "karma-sinon": "^1.0.5", + "karma-sourcemap-loader": "^0.4.0", + "lint-staged": "^15.2.10", + "memoizee": "^0.4.17", + "minimist": "^1.2.8", + "mocha": "^10.8.2", + "multer": "^1.4.4", + "pacote": "^20.0.0", + "playwright": "^1.58.2", + "prettier": "^3.8.1", + "pretty-bytes": "^6.1.1", + "rollup": "^2.79.2", + "rollup-plugin-auto-external": "^2.0.0", + "rollup-plugin-bundle-size": "^1.0.3", + "rollup-plugin-terser": "^7.0.2", + "selfsigned": "^3.0.1", + "sinon": "^4.5.0", + "stream-throttle": "^0.1.3", + "string-replace-async": "^3.0.2", + "tar-stream": "^3.1.7", + "typescript": "^4.9.5", + "vitest": "^4.0.18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/axios": { + "resolved": "../../..", + "link": true + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/tests/smoke/esm/package.json b/tests/smoke/esm/package.json new file mode 100644 index 00000000..186eecf7 --- /dev/null +++ b/tests/smoke/esm/package.json @@ -0,0 +1,18 @@ +{ + "name": "@axios/esm-smoke-tests", + "version": "1.0.0", + "description": "ESM smoke tests for axios", + "private": true, + "scripts": { + "test:smoke:esm:vitest": "vitest run --config vitest.config.js --project smoke" + }, + "keywords": [], + "author": "axios team", + "license": "MIT", + "dependencies": { + "axios": "file:../../../" + }, + "devDependencies": { + "vitest": "4.0.18" + } +} diff --git a/tests/smoke/esm/tests/auth.smoke.test.js b/tests/smoke/esm/tests/auth.smoke.test.js new file mode 100644 index 00000000..40f8bca1 --- /dev/null +++ b/tests/smoke/esm/tests/auth.smoke.test.js @@ -0,0 +1,137 @@ +import { afterEach, describe, expect, it } from 'vitest'; + +import http from 'http'; +import axios from 'axios'; + +const startServer = (handler) => { + return new Promise((resolve) => { + const server = http.createServer(handler); + + server.listen(0, '127.0.0.1', () => { + resolve(server); + }); + }); +}; + +const stopServer = (server) => { + if (!server || !server.listening) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); +}; + +describe('auth compat (dist export only)', () => { + let server; + + afterEach(async () => { + await stopServer(server); + server = undefined; + }); + + const requestWithConfig = async (config) => { + server = await startServer((req, res) => { + res.setHeader('Content-Type', 'text/plain'); + res.end(req.headers.authorization || ''); + }); + + const { port } = server.address(); + + return axios.get( + `http://127.0.0.1:${port}/`, + Object.assign( + { + proxy: false, + }, + config || {} + ) + ); + }; + + it('sets Basic Authorization header from auth credentials', async () => { + const response = await requestWithConfig({ + auth: { + username: 'janedoe', + password: 's00pers3cret', + }, + }); + + const expected = `Basic ${Buffer.from('janedoe:s00pers3cret', 'utf8').toString('base64')}`; + + expect(response.data).toBe(expected); + }); + + it('supports auth without password', async () => { + const response = await requestWithConfig({ + auth: { + username: 'Aladdin', + }, + }); + + const expected = `Basic ${Buffer.from('Aladdin:', 'utf8').toString('base64')}`; + + expect(response.data).toBe(expected); + }); + + it('overwrites an existing Authorization header when auth is provided', async () => { + const response = await requestWithConfig({ + headers: { + Authorization: 'Bearer token-123', + }, + auth: { + username: 'foo', + password: 'bar', + }, + }); + + const expected = `Basic ${Buffer.from('foo:bar', 'utf8').toString('base64')}`; + + expect(response.data).toBe(expected); + }); + + it('uses URL credentials when auth config is not provided (node adapter behavior)', async () => { + server = await startServer((req, res) => { + res.setHeader('Content-Type', 'text/plain'); + res.end(req.headers.authorization || ''); + }); + + const { port } = server.address(); + + const response = await axios.get(`http://urluser:urlpass@127.0.0.1:${port}/`, { + proxy: false, + }); + + const expected = `Basic ${Buffer.from('urluser:urlpass', 'utf8').toString('base64')}`; + + expect(response.data).toBe(expected); + }); + + it('prefers auth config over URL credentials', async () => { + server = await startServer((req, res) => { + res.setHeader('Content-Type', 'text/plain'); + res.end(req.headers.authorization || ''); + }); + + const { port } = server.address(); + + const response = await axios.get(`http://urluser:urlpass@127.0.0.1:${port}/`, { + proxy: false, + auth: { + username: 'configuser', + password: 'configpass', + }, + }); + + const expected = `Basic ${Buffer.from('configuser:configpass', 'utf8').toString('base64')}`; + + expect(response.data).toBe(expected); + }); +}); diff --git a/tests/smoke/esm/tests/basic.smoke.test.js b/tests/smoke/esm/tests/basic.smoke.test.js new file mode 100644 index 00000000..ea7003f6 --- /dev/null +++ b/tests/smoke/esm/tests/basic.smoke.test.js @@ -0,0 +1,145 @@ +import { describe, expect, it } from 'vitest'; + +import { EventEmitter } from 'events'; +import { PassThrough } from 'stream'; +import axios from 'axios'; + +const createTransportCapture = () => { + let capturedOptions; + + const transport = { + request(options, onResponse) { + capturedOptions = options; + + const req = new EventEmitter(); + req.destroyed = false; + req.setTimeout = () => {}; + req.write = () => true; + req.end = () => { + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = { 'content-type': 'application/json' }; + res.req = req; + onResponse(res); + res.end('{"ok":true}'); + }; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + + return req; + }, + }; + + return { + transport, + getCapturedOptions: () => capturedOptions, + }; +}; + +const runRequest = async (run) => { + const { transport, getCapturedOptions } = createTransportCapture(); + await run(transport); + + return getCapturedOptions(); +}; + +describe('basic compat (dist export only)', () => { + it('supports the simplest axios(url) request pattern', async () => { + const options = await runRequest((transport) => + axios('http://example.com/users', { + transport, + proxy: false, + }) + ); + + expect(options.method).toBe('GET'); + expect(options.path).toBe('/users'); + }); + + it('supports get()', async () => { + const options = await runRequest((transport) => + axios.get('http://example.com/items?limit=10', { transport, proxy: false }) + ); + + expect(options.method).toBe('GET'); + expect(options.path).toBe('/items?limit=10'); + }); + + it('supports delete()', async () => { + const options = await runRequest((transport) => + axios.delete('http://example.com/items/1', { transport, proxy: false }) + ); + + expect(options.method).toBe('DELETE'); + expect(options.path).toBe('/items/1'); + }); + + it('supports head()', async () => { + const options = await runRequest((transport) => + axios.head('http://example.com/health', { transport, proxy: false }) + ); + + expect(options.method).toBe('HEAD'); + expect(options.path).toBe('/health'); + }); + + it('supports options()', async () => { + const options = await runRequest((transport) => + axios.options('http://example.com/items', { transport, proxy: false }) + ); + + expect(options.method).toBe('OPTIONS'); + expect(options.path).toBe('/items'); + }); + + it('supports post()', async () => { + const options = await runRequest((transport) => + axios.post( + 'http://example.com/items', + { name: 'widget' }, + { + transport, + proxy: false, + } + ) + ); + + expect(options.method).toBe('POST'); + expect(options.path).toBe('/items'); + }); + + it('supports put()', async () => { + const options = await runRequest((transport) => + axios.put( + 'http://example.com/items/1', + { name: 'updated-widget' }, + { + transport, + proxy: false, + } + ) + ); + + expect(options.method).toBe('PUT'); + expect(options.path).toBe('/items/1'); + }); + + it('supports patch()', async () => { + const options = await runRequest((transport) => + axios.patch( + 'http://example.com/items/1', + { status: 'active' }, + { + transport, + proxy: false, + } + ) + ); + + expect(options.method).toBe('PATCH'); + expect(options.path).toBe('/items/1'); + }); +}); diff --git a/tests/smoke/esm/tests/cancel.smoke.test.js b/tests/smoke/esm/tests/cancel.smoke.test.js new file mode 100644 index 00000000..dee2b83b --- /dev/null +++ b/tests/smoke/esm/tests/cancel.smoke.test.js @@ -0,0 +1,111 @@ +import { describe, expect, it } from 'vitest'; + +import { EventEmitter } from 'events'; +import axios from 'axios'; + +const createPendingTransport = () => { + let requestCount = 0; + + const transport = { + request() { + requestCount += 1; + + const req = new EventEmitter(); + req.destroyed = false; + req.setTimeout = () => {}; + req.write = () => true; + req.end = () => {}; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + + return req; + }, + }; + + return { + transport, + getRequestCount: () => requestCount, + }; +}; + +describe('cancel compat (dist export only)', () => { + it('supports cancellation with AbortController (pre-aborted signal)', async () => { + const { transport, getRequestCount } = createPendingTransport(); + const controller = new AbortController(); + controller.abort(); + + try { + const request = axios.get('http://example.com/resource', { + signal: controller.signal, + transport, + proxy: false, + }); + + controller.abort(); + await request; + } catch (error) { + expect(error.code).toBe('ERR_CANCELED'); + } + + expect(getRequestCount()).toBe(0); + }); + + it('supports cancellation with AbortController (in-flight)', async () => { + const { transport, getRequestCount } = createPendingTransport(); + const controller = new AbortController(); + + try { + const request = axios.get('http://example.com/resource', { + signal: controller.signal, + transport, + proxy: false, + }); + + controller.abort(); + await request; + } catch (error) { + expect(error.code).toBe('ERR_CANCELED'); + } + + expect(getRequestCount()).toBe(1); + }); + + it('supports cancellation with CancelToken (pre-canceled token)', async () => { + const { transport, getRequestCount } = createPendingTransport(); + const source = axios.CancelToken.source(); + source.cancel('Operation canceled by the user.'); + + const error = await axios + .get('http://example.com/resource', { + cancelToken: source.token, + transport, + proxy: false, + }) + .catch((err) => err); + + expect(axios.isCancel(error)).toBe(true); + expect(error.code).toBe('ERR_CANCELED'); + expect(getRequestCount()).toBe(0); + }); + + it('supports cancellation with CancelToken (in-flight)', async () => { + const { transport, getRequestCount } = createPendingTransport(); + const source = axios.CancelToken.source(); + + const request = axios.get('http://example.com/resource', { + cancelToken: source.token, + transport, + proxy: false, + }); + + source.cancel('Operation canceled by the user.'); + + const error = await request.catch((err) => err); + + expect(axios.isCancel(error)).toBe(true); + expect(error.code).toBe('ERR_CANCELED'); + expect(getRequestCount()).toBe(1); + }); +}); diff --git a/tests/smoke/esm/tests/error.smoke.test.js b/tests/smoke/esm/tests/error.smoke.test.js new file mode 100644 index 00000000..3a23e048 --- /dev/null +++ b/tests/smoke/esm/tests/error.smoke.test.js @@ -0,0 +1,139 @@ +import { describe, expect, it } from 'vitest'; + +import { EventEmitter } from 'events'; +import { PassThrough } from 'stream'; +import axios from 'axios'; + +const createTransport = (config) => { + const opts = config || {}; + + return { + request(options, onResponse) { + const req = new EventEmitter(); + req.destroyed = false; + req.write = () => true; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + + req.setTimeout = (_ms, cb) => { + if (opts.timeout) { + req._timeoutCallback = cb; + } + }; + + req.end = () => { + if (opts.error) { + req.emit('error', opts.error); + return; + } + + if (opts.timeout && req._timeoutCallback) { + req._timeoutCallback(); + return; + } + + const res = new PassThrough(); + res.statusCode = + opts.response && opts.response.statusCode !== undefined ? opts.response.statusCode : 200; + res.statusMessage = + opts.response && opts.response.statusMessage ? opts.response.statusMessage : 'OK'; + res.headers = + opts.response && opts.response.headers + ? opts.response.headers + : { 'content-type': 'application/json' }; + res.req = req; + + onResponse(res); + res.end( + opts.response && opts.response.body !== undefined ? opts.response.body : '{"ok":true}' + ); + }; + + return req; + }, + }; +}; + +describe('error compat (dist export only)', () => { + it('rejects with AxiosError for non-2xx responses by default', async () => { + const err = await axios + .get('http://example.com/fail', { + proxy: false, + transport: createTransport({ + response: { + statusCode: 500, + statusMessage: 'Internal Server Error', + body: '{"error":"boom"}', + }, + }), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).toBe(true); + expect(err.response.status).toBe(500); + expect(err.message).toContain('500'); + }); + + it('resolves when validateStatus allows non-2xx responses', async () => { + const response = await axios.get('http://example.com/allowed', { + proxy: false, + validateStatus: () => true, + transport: createTransport({ + response: { + statusCode: 500, + statusMessage: 'Internal Server Error', + body: '{"ok":false}', + }, + }), + }); + + expect(response.status).toBe(500); + expect(response.data).toEqual({ ok: false }); + }); + + it('wraps transport errors as AxiosError', async () => { + const err = await axios + .get('http://example.com/network', { + proxy: false, + transport: createTransport({ + error: new Error('socket hang up'), + }), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).toBe(true); + expect(err.message).toContain('socket hang up'); + expect(err.toJSON).toBeTypeOf('function'); + }); + + it('rejects with ECONNABORTED on timeout', async () => { + const err = await axios + .get('http://example.com/timeout', { + proxy: false, + timeout: 10, + transport: createTransport({ timeout: true }), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).toBe(true); + expect(err.code).toBe('ECONNABORTED'); + expect(err.message).toBe('timeout of 10ms exceeded'); + }); + + it('uses timeoutErrorMessage when provided', async () => { + const err = await axios + .get('http://example.com/timeout', { + proxy: false, + timeout: 25, + timeoutErrorMessage: 'custom timeout message', + transport: createTransport({ timeout: true }), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).toBe(true); + expect(err.code).toBe('ECONNABORTED'); + expect(err.message).toBe('custom timeout message'); + }); +}); diff --git a/tests/smoke/esm/tests/fetch.smoke.test.js b/tests/smoke/esm/tests/fetch.smoke.test.js new file mode 100644 index 00000000..2c7f557b --- /dev/null +++ b/tests/smoke/esm/tests/fetch.smoke.test.js @@ -0,0 +1,142 @@ +import { describe, expect, it } from 'vitest'; + +import axios from 'axios'; + +const createFetchMock = (responseFactory) => { + const calls = []; + + const mockFetch = async (input, init) => { + calls.push({ input, init: init || {} }); + + if (responseFactory) { + return responseFactory(input, init || {}); + } + + return new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }; + + return { + mockFetch, + getCalls: () => calls, + }; +}; + +describe('fetch compat (dist export only)', () => { + it('uses fetch adapter and resolves JSON response', async () => { + const { mockFetch, getCalls } = createFetchMock(); + + const response = await axios.get('https://example.com/users', { + adapter: 'fetch', + env: { + fetch: mockFetch, + Request, + Response, + }, + }); + + expect(response.data).toEqual({ ok: true }); + expect(response.status).toBe(200); + expect(getCalls()).toHaveLength(1); + }); + + it('sends method, headers and body for post requests', async () => { + const { mockFetch, getCalls } = createFetchMock(async (input, init) => { + const requestInit = init || {}; + const isRequest = input && typeof input !== 'string'; + const method = isRequest ? input.method : requestInit.method; + + const body = + isRequest && typeof input.clone === 'function' + ? await input.clone().text() + : requestInit.body; + + let contentType; + if (isRequest && input.headers) { + contentType = input.headers.get('content-type'); + } else if (requestInit.headers) { + contentType = requestInit.headers['Content-Type'] || requestInit.headers['content-type']; + } + + return new Response( + JSON.stringify({ + url: typeof input === 'string' ? input : input.url, + method, + contentType, + body, + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + } + ); + }); + + const response = await axios.post( + 'https://example.com/items', + { name: 'widget' }, + { + adapter: 'fetch', + env: { + fetch: mockFetch, + Request, + Response, + }, + } + ); + + expect(getCalls()).toHaveLength(1); + expect(response.data.url).toBe('https://example.com/items'); + expect(response.data.method).toBe('POST'); + expect(response.data.contentType).toContain('application/json'); + expect(response.data.body).toBe(JSON.stringify({ name: 'widget' })); + }); + + it('rejects non-2xx fetch responses by default', async () => { + const { mockFetch } = createFetchMock( + () => + new Response(JSON.stringify({ error: 'boom' }), { + status: 500, + statusText: 'Internal Server Error', + headers: { 'Content-Type': 'application/json' }, + }) + ); + + const err = await axios + .get('https://example.com/fail', { + adapter: 'fetch', + env: { + fetch: mockFetch, + Request, + Response, + }, + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).toBe(true); + expect(err.response.status).toBe(500); + }); + + it('supports cancellation with AbortController in fetch mode', async () => { + const { mockFetch } = createFetchMock(); + const controller = new AbortController(); + controller.abort(); + + const err = await axios + .get('https://example.com/cancel', { + adapter: 'fetch', + signal: controller.signal, + env: { + fetch: mockFetch, + Request, + Response, + }, + }) + .catch((e) => e); + + expect(axios.isCancel(err)).toBe(true); + expect(err.code).toBe('ERR_CANCELED'); + }); +}); diff --git a/tests/smoke/esm/tests/files.smoke.test.js b/tests/smoke/esm/tests/files.smoke.test.js new file mode 100644 index 00000000..006c62b4 --- /dev/null +++ b/tests/smoke/esm/tests/files.smoke.test.js @@ -0,0 +1,115 @@ +import { describe, expect, it } from 'vitest'; + +import { PassThrough, Readable, Writable } from 'stream'; +import axios from 'axios'; + +const createCaptureTransport = (buildResponse) => { + return { + request(options, onResponse) { + const chunks = []; + + const req = new Writable({ + write(chunk, _encoding, callback) { + chunks.push(Buffer.from(chunk)); + callback(); + }, + }); + + req.destroyed = false; + req.setTimeout = () => {}; + req.close = () => { + req.destroyed = true; + }; + + const originalDestroy = req.destroy.bind(req); + req.destroy = (...args) => { + req.destroyed = true; + return originalDestroy(...args); + }; + + const originalEnd = req.end.bind(req); + req.end = (...args) => { + originalEnd(...args); + + const body = Buffer.concat(chunks); + const response = buildResponse ? buildResponse(body, options) : {}; + + const res = new PassThrough(); + res.statusCode = response.statusCode !== undefined ? response.statusCode : 200; + res.statusMessage = response.statusMessage || 'OK'; + res.headers = response.headers || { 'content-type': 'application/json' }; + res.req = req; + + onResponse(res); + res.end(response.body || JSON.stringify({ size: body.length })); + }; + + req.on('error', () => {}); + return req; + }, + }; +}; + +describe('files compat (dist export only)', () => { + it('supports posting Buffer payloads', async () => { + const source = Buffer.from('binary-\x00-data', 'utf8'); + + const response = await axios.post('http://example.com/upload', source, { + proxy: false, + transport: createCaptureTransport((body) => ({ + body: JSON.stringify({ echoed: body.toString('base64') }), + })), + }); + + expect(response.data.echoed).toBe(source.toString('base64')); + }); + + it('supports posting Uint8Array payloads', async () => { + const source = Uint8Array.from([1, 2, 3, 4, 255]); + + const response = await axios.post('http://example.com/upload', source, { + proxy: false, + transport: createCaptureTransport((body) => ({ + body: JSON.stringify({ echoed: Array.from(body.values()) }), + })), + }); + + expect(response.data.echoed).toEqual([1, 2, 3, 4, 255]); + }); + + it('supports posting Readable stream payloads', async () => { + const streamData = ['hello ', 'stream ', 'world']; + const source = Readable.from(streamData); + + const response = await axios.post('http://example.com/upload', source, { + proxy: false, + headers: { 'Content-Type': 'application/octet-stream' }, + transport: createCaptureTransport((body, options) => ({ + body: JSON.stringify({ + text: body.toString('utf8'), + contentType: + options.headers && (options.headers['Content-Type'] || options.headers['content-type']), + }), + })), + }); + + expect(response.data.text).toBe('hello stream world'); + expect(response.data.contentType).toContain('application/octet-stream'); + }); + + it('supports binary downloads with responseType=arraybuffer', async () => { + const binary = Buffer.from([0xde, 0xad, 0xbe, 0xef]); + + const response = await axios.get('http://example.com/file.bin', { + proxy: false, + responseType: 'arraybuffer', + transport: createCaptureTransport(() => ({ + headers: { 'content-type': 'application/octet-stream' }, + body: binary, + })), + }); + + expect(Buffer.isBuffer(response.data)).toBe(true); + expect(response.data.equals(binary)).toBe(true); + }); +}); diff --git a/tests/smoke/esm/tests/formData.smoke.test.js b/tests/smoke/esm/tests/formData.smoke.test.js new file mode 100644 index 00000000..6b375ff9 --- /dev/null +++ b/tests/smoke/esm/tests/formData.smoke.test.js @@ -0,0 +1,107 @@ +import { describe, expect, it } from 'vitest'; + +import { Writable, PassThrough } from 'stream'; +import axios from 'axios'; + +const createCaptureTransport = (buildResponse) => { + return { + request(options, onResponse) { + const chunks = []; + + const req = new Writable({ + write(chunk, _encoding, callback) { + chunks.push(Buffer.from(chunk)); + callback(); + }, + }); + + req.destroyed = false; + req.setTimeout = () => {}; + req.write = req.write.bind(req); + req.close = () => { + req.destroyed = true; + }; + + const originalDestroy = req.destroy.bind(req); + req.destroy = (...args) => { + req.destroyed = true; + return originalDestroy(...args); + }; + + const originalEnd = req.end.bind(req); + req.end = (...args) => { + originalEnd(...args); + + const body = Buffer.concat(chunks); + const response = buildResponse ? buildResponse(body, options) : {}; + + const res = new PassThrough(); + res.statusCode = response.statusCode !== undefined ? response.statusCode : 200; + res.statusMessage = response.statusMessage || 'OK'; + res.headers = response.headers || { 'content-type': 'application/json' }; + res.req = req; + + onResponse(res); + res.end(response.body || JSON.stringify({ ok: true })); + }; + + return req; + }, + }; +}; + +const bodyAsUtf8 = (value) => { + return Buffer.isBuffer(value) ? value.toString('utf8') : String(value); +}; + +describe('formData compat (dist export only)', () => { + it('supports posting FormData instances', async () => { + const form = new FormData(); + form.append('username', 'janedoe'); + form.append('role', 'admin'); + + const response = await axios.post('http://example.com/form', form, { + proxy: false, + transport: createCaptureTransport((body, options) => ({ + body: JSON.stringify({ + contentType: + options.headers && (options.headers['Content-Type'] || options.headers['content-type']), + payload: bodyAsUtf8(body), + }), + })), + }); + + expect(response.data.contentType).toContain('multipart/form-data'); + expect(response.data.payload).toContain('name="username"'); + expect(response.data.payload).toContain('janedoe'); + expect(response.data.payload).toContain('name="role"'); + expect(response.data.payload).toContain('admin'); + }); + + it('supports axios.postForm helper', async () => { + const response = await axios.postForm( + 'http://example.com/post-form', + { + project: 'axios', + mode: 'compat', + }, + { + proxy: false, + transport: createCaptureTransport((body, options) => ({ + body: JSON.stringify({ + contentType: + options.headers && + (options.headers['Content-Type'] || options.headers['content-type']), + payload: bodyAsUtf8(body), + }), + })), + } + ); + + expect(response.data.contentType).toContain('multipart/form-data'); + expect(response.data.payload).toContain('name="project"'); + expect(response.data.payload).toContain('axios'); + expect(response.data.payload).toContain('name="mode"'); + expect(response.data.payload).toContain('compat'); + }); +}); diff --git a/tests/smoke/esm/tests/headers.smoke.test.js b/tests/smoke/esm/tests/headers.smoke.test.js new file mode 100644 index 00000000..2065e032 --- /dev/null +++ b/tests/smoke/esm/tests/headers.smoke.test.js @@ -0,0 +1,125 @@ +import { describe, expect, it } from 'vitest'; + +import { EventEmitter } from 'events'; +import { PassThrough } from 'stream'; +import axios from 'axios'; + +const normalizeHeaders = (headers) => { + const result = {}; + + Object.entries(headers || {}).forEach(([key, value]) => { + result[key.toLowerCase()] = value; + }); + + return result; +}; + +const createTransportCapture = () => { + let capturedOptions; + + const transport = { + request(options, onResponse) { + capturedOptions = options; + + const req = new EventEmitter(); + req.destroyed = false; + req.setTimeout = () => {}; + req.write = () => true; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + req.end = () => { + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = { 'content-type': 'application/json' }; + res.req = req; + onResponse(res); + res.end('{"ok":true}'); + }; + + return req; + }, + }; + + return { + transport, + getCapturedOptions: () => capturedOptions, + }; +}; + +describe('headers compat (dist export only)', () => { + it('sends default Accept header', async () => { + const { transport, getCapturedOptions } = createTransportCapture(); + + await axios.get('http://example.com/default-headers', { + transport, + proxy: false, + }); + + const headers = normalizeHeaders(getCapturedOptions().headers); + expect(headers.accept).toBe('application/json, text/plain, */*'); + }); + + it('supports custom headers', async () => { + const { transport, getCapturedOptions } = createTransportCapture(); + + await axios.get('http://example.com/custom-headers', { + transport, + proxy: false, + headers: { + 'X-Trace-Id': 'trace-123', + Authorization: 'Bearer token-abc', + }, + }); + + const headers = normalizeHeaders(getCapturedOptions().headers); + expect(headers['x-trace-id']).toBe('trace-123'); + expect(headers.authorization).toBe('Bearer token-abc'); + }); + + it('treats header names as case-insensitive when overriding', async () => { + const { transport, getCapturedOptions } = createTransportCapture(); + + await axios.get('http://example.com/case-insensitive', { + transport, + proxy: false, + headers: { + authorization: 'Bearer old-token', + AuThOrIzAtIoN: 'Bearer new-token', + }, + }); + + const headers = normalizeHeaders(getCapturedOptions().headers); + expect(headers.authorization).toBe('Bearer new-token'); + }); + + it('sets content-type for json post payloads', async () => { + const { transport, getCapturedOptions } = createTransportCapture(); + + await axios.post( + 'http://example.com/post-json', + { name: 'widget' }, + { + transport, + proxy: false, + } + ); + + const headers = normalizeHeaders(getCapturedOptions().headers); + expect(headers['content-type']).toContain('application/json'); + }); + + it('does not force content-type for get requests without body', async () => { + const { transport, getCapturedOptions } = createTransportCapture(); + + await axios.get('http://example.com/get-no-body', { + transport, + proxy: false, + }); + + const headers = normalizeHeaders(getCapturedOptions().headers); + expect(headers['content-type']).toBeUndefined(); + }); +}); diff --git a/tests/smoke/esm/tests/http2.smoke.test.js b/tests/smoke/esm/tests/http2.smoke.test.js new file mode 100644 index 00000000..c19174b0 --- /dev/null +++ b/tests/smoke/esm/tests/http2.smoke.test.js @@ -0,0 +1,86 @@ +import { describe, expect, it } from 'vitest'; + +import axios from 'axios'; + +describe('http2 compat (dist export only)', () => { + it('keeps instance-level httpVersion and http2Options in request config', async () => { + const client = axios.create({ + baseURL: 'https://example.com', + httpVersion: 2, + http2Options: { + rejectUnauthorized: false, + }, + }); + + const response = await client.get('/resource', { + adapter: async (config) => ({ + data: { + httpVersion: config.httpVersion, + http2Options: config.http2Options, + }, + status: 200, + statusText: 'OK', + headers: {}, + config, + }), + }); + + expect(response.data.httpVersion).toBe(2); + expect(response.data.http2Options).toEqual({ + rejectUnauthorized: false, + }); + }); + + it('merges request http2Options with instance http2Options', async () => { + const client = axios.create({ + baseURL: 'https://example.com', + httpVersion: 2, + http2Options: { + rejectUnauthorized: false, + sessionTimeout: 1000, + }, + }); + + const response = await client.get('/resource', { + http2Options: { + sessionTimeout: 5000, + customFlag: true, + }, + adapter: async (config) => ({ + data: config.http2Options, + status: 200, + statusText: 'OK', + headers: {}, + config, + }), + }); + + expect(response.data).toEqual({ + rejectUnauthorized: false, + sessionTimeout: 5000, + customFlag: true, + }); + }); + + it('allows request-level httpVersion override', async () => { + const client = axios.create({ + baseURL: 'https://example.com', + httpVersion: 2, + }); + + const response = await client.get('/resource', { + httpVersion: 1, + adapter: async (config) => ({ + data: { + httpVersion: config.httpVersion, + }, + status: 200, + statusText: 'OK', + headers: {}, + config, + }), + }); + + expect(response.data.httpVersion).toBe(1); + }); +}); diff --git a/tests/smoke/esm/tests/instance.smoke.test.js b/tests/smoke/esm/tests/instance.smoke.test.js new file mode 100644 index 00000000..688ffe28 --- /dev/null +++ b/tests/smoke/esm/tests/instance.smoke.test.js @@ -0,0 +1,146 @@ +import { describe, expect, it } from 'vitest'; + +import { EventEmitter } from 'events'; +import { PassThrough } from 'stream'; +import axios from 'axios'; + +const createTransportCapture = (responseBody) => { + const calls = []; + + const transport = { + request(options, onResponse) { + calls.push(options); + + const req = new EventEmitter(); + req.destroyed = false; + req.setTimeout = () => {}; + req.write = () => true; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + req.end = () => { + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = { 'content-type': 'application/json' }; + res.req = req; + onResponse(res); + res.end(responseBody ? responseBody : '{"ok":true}'); + }; + + return req; + }, + }; + + return { + transport, + getCalls: () => calls, + }; +}; + +describe('instance compat (dist export only)', () => { + it('creates isolated instances with separate defaults', async () => { + const { transport, getCalls } = createTransportCapture(); + + const clientA = axios.create({ + baseURL: 'http://example.com/api-a', + headers: { + 'X-App': 'A', + }, + }); + const clientB = axios.create({ + baseURL: 'http://example.com/api-b', + headers: { + 'X-App': 'B', + }, + }); + + await clientA.get('/users', { transport, proxy: false }); + await clientB.get('/users', { transport, proxy: false }); + + const [callA, callB] = getCalls(); + expect(callA.path).toBe('/api-a/users'); + expect(callB.path).toBe('/api-b/users'); + expect(callA.headers['X-App']).toBe('A'); + expect(callB.headers['X-App']).toBe('B'); + }); + + it('supports callable instance form instance(config)', async () => { + const { transport, getCalls } = createTransportCapture(); + const client = axios.create({ + baseURL: 'http://example.com', + }); + + await client({ + url: '/status', + method: 'get', + transport, + proxy: false, + }); + + expect(getCalls()).toHaveLength(1); + expect(getCalls()[0].method).toBe('GET'); + expect(getCalls()[0].path).toBe('/status'); + }); + + it('applies instance request interceptors', async () => { + const { transport, getCalls } = createTransportCapture(); + const client = axios.create({ + baseURL: 'http://example.com', + }); + + client.interceptors.request.use((config) => { + config.headers = config.headers || {}; + config.headers['X-From-Interceptor'] = 'yes'; + return config; + }); + + await client.get('/intercepted', { transport, proxy: false }); + + expect(getCalls()).toHaveLength(1); + expect(getCalls()[0].headers['X-From-Interceptor']).toBe('yes'); + }); + + it('applies instance response interceptors', async () => { + const { transport } = createTransportCapture('{"name":"axios"}'); + const client = axios.create({ + baseURL: 'http://example.com', + }); + + client.interceptors.response.use((response) => { + response.data = Object.assign({}, response.data, { + intercepted: true, + }); + return response; + }); + + const response = await client.get('/response-interceptor', { + transport, + proxy: false, + }); + + expect(response.data).toEqual({ + name: 'axios', + intercepted: true, + }); + }); + + it('builds URLs with getUri from instance defaults and request params', () => { + const client = axios.create({ + baseURL: 'http://example.com/api', + params: { + apiKey: 'abc', + }, + }); + + const uri = client.getUri({ + url: '/users', + params: { + page: 2, + }, + }); + + expect(uri).toBe('http://example.com/api/users?apiKey=abc&page=2'); + }); +}); diff --git a/tests/smoke/esm/tests/interceptors.smoke.test.js b/tests/smoke/esm/tests/interceptors.smoke.test.js new file mode 100644 index 00000000..d91a405f --- /dev/null +++ b/tests/smoke/esm/tests/interceptors.smoke.test.js @@ -0,0 +1,149 @@ +import { describe, expect, it } from 'vitest'; + +import { EventEmitter } from 'events'; +import { PassThrough } from 'stream'; +import axios from 'axios'; + +const createTransport = (responseBody) => { + const calls = []; + + const transport = { + request(options, onResponse) { + calls.push(options); + + const req = new EventEmitter(); + req.destroyed = false; + req.setTimeout = () => {}; + req.write = () => true; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + req.end = () => { + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = { 'content-type': 'application/json' }; + res.req = req; + onResponse(res); + res.end(responseBody ? responseBody : '{"value":"ok"}'); + }; + + return req; + }, + }; + + return { + transport, + getCalls: () => calls, + }; +}; + +describe('interceptors compat (dist export only)', () => { + it('applies request interceptors before dispatch', async () => { + const { transport, getCalls } = createTransport(); + const client = axios.create(); + + client.interceptors.request.use((config) => { + config.headers = config.headers || {}; + config.headers['X-One'] = '1'; + return config; + }); + + client.interceptors.request.use((config) => { + config.headers['X-Two'] = '2'; + return config; + }); + + await client.get('http://example.com/resource', { + transport, + proxy: false, + }); + + expect(getCalls()).toHaveLength(1); + expect(getCalls()[0].headers['X-One']).toBe('1'); + expect(getCalls()[0].headers['X-Two']).toBe('2'); + }); + + it('applies response interceptors in registration order', async () => { + const { transport } = createTransport('{"n":1}'); + const client = axios.create(); + + client.interceptors.response.use((response) => { + response.data.n += 1; + return response; + }); + + client.interceptors.response.use((response) => { + response.data.n *= 10; + return response; + }); + + const response = await client.get('http://example.com/resource', { + transport, + proxy: false, + }); + + expect(response.data.n).toBe(20); + }); + + it('supports ejecting request interceptors', async () => { + const { transport, getCalls } = createTransport(); + const client = axios.create(); + + const id = client.interceptors.request.use((config) => { + config.headers = config.headers || {}; + config.headers['X-Ejected'] = 'yes'; + return config; + }); + + client.interceptors.request.eject(id); + + await client.get('http://example.com/resource', { + transport, + proxy: false, + }); + + expect(getCalls()).toHaveLength(1); + expect(getCalls()[0].headers['X-Ejected']).toBeUndefined(); + }); + + it('supports async request interceptors', async () => { + const { transport, getCalls } = createTransport(); + const client = axios.create(); + + client.interceptors.request.use(async (config) => { + await Promise.resolve(); + config.headers = config.headers || {}; + config.headers['X-Async'] = 'true'; + return config; + }); + + await client.get('http://example.com/resource', { + transport, + proxy: false, + }); + + expect(getCalls()[0].headers['X-Async']).toBe('true'); + }); + + it('propagates errors thrown by request interceptors', async () => { + const { transport, getCalls } = createTransport(); + const client = axios.create(); + + client.interceptors.request.use(() => { + throw new Error('blocked-by-interceptor'); + }); + + const err = await client + .get('http://example.com/resource', { + transport, + proxy: false, + }) + .catch((e) => e); + + expect(err).toBeInstanceOf(Error); + expect(err.message).toContain('blocked-by-interceptor'); + expect(getCalls()).toHaveLength(0); + }); +}); diff --git a/tests/smoke/esm/tests/progress.smoke.test.js b/tests/smoke/esm/tests/progress.smoke.test.js new file mode 100644 index 00000000..123a768c --- /dev/null +++ b/tests/smoke/esm/tests/progress.smoke.test.js @@ -0,0 +1,113 @@ +import { describe, expect, it } from 'vitest'; + +import { Readable, Writable, PassThrough } from 'stream'; +import axios from 'axios'; + +const createProgressTransport = (config) => { + const opts = config || {}; + const responseChunks = opts.responseChunks || ['ok']; + const responseHeaders = opts.responseHeaders || {}; + + return { + request(_options, onResponse) { + const req = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + }); + + req.destroyed = false; + req.setTimeout = () => {}; + req.close = () => { + req.destroyed = true; + }; + + const originalDestroy = req.destroy.bind(req); + req.destroy = (...args) => { + req.destroyed = true; + return originalDestroy(...args); + }; + + const originalEnd = req.end.bind(req); + req.end = (...args) => { + originalEnd(...args); + + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = Object.assign( + { + 'content-type': 'text/plain', + }, + responseHeaders + ); + res.req = req; + + onResponse(res); + + responseChunks.forEach((chunk) => { + res.write(chunk); + }); + res.end(); + }; + + return req; + }, + }; +}; + +describe('progress compat (dist export only)', () => { + it('emits upload progress events for stream payloads', async () => { + const samples = []; + const payload = ['abc', 'def', 'ghi']; + const total = payload.join('').length; + + await axios.post('http://example.com/upload', Readable.from(payload), { + proxy: false, + headers: { + 'Content-Length': String(total), + }, + onUploadProgress: ({ loaded, total: reportedTotal, upload }) => { + samples.push({ loaded, total: reportedTotal, upload }); + }, + transport: createProgressTransport({ + responseChunks: ['uploaded'], + }), + }); + + expect(samples.length).toBeGreaterThan(0); + expect(samples[samples.length - 1]).toMatchObject({ + loaded: total, + total, + upload: true, + }); + }); + + it('emits download progress events', async () => { + const samples = []; + const chunks = ['ab', 'cd', 'ef']; + const total = chunks.join('').length; + + const response = await axios.get('http://example.com/download', { + proxy: false, + responseType: 'text', + onDownloadProgress: ({ loaded, total: reportedTotal, download }) => { + samples.push({ loaded, total: reportedTotal, download }); + }, + transport: createProgressTransport({ + responseChunks: chunks, + responseHeaders: { + 'content-length': String(total), + }, + }), + }); + + expect(response.data).toBe('abcdef'); + expect(samples.length).toBeGreaterThan(0); + expect(samples[samples.length - 1]).toMatchObject({ + loaded: total, + total, + download: true, + }); + }); +}); diff --git a/tests/smoke/esm/tests/rateLimit.smoke.test.js b/tests/smoke/esm/tests/rateLimit.smoke.test.js new file mode 100644 index 00000000..bcdd08d2 --- /dev/null +++ b/tests/smoke/esm/tests/rateLimit.smoke.test.js @@ -0,0 +1,105 @@ +import { describe, expect, it } from 'vitest'; + +import { EventEmitter } from 'events'; +import { PassThrough } from 'stream'; +import axios from 'axios'; + +const createTransportCapture = () => { + let capturedOptions; + + const transport = { + request(options, onResponse) { + capturedOptions = options; + + const req = new EventEmitter(); + req.destroyed = false; + req.setTimeout = () => {}; + req.write = () => true; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + req.end = () => { + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = { 'content-type': 'application/json' }; + res.req = req; + onResponse(res); + res.end('{"ok":true}'); + }; + + return req; + }, + }; + + return { + transport, + getCapturedOptions: () => capturedOptions, + }; +}; + +describe('rateLimit compat (dist export only)', () => { + it('accepts numeric maxRate config', async () => { + const response = await axios.get('http://example.com/rate', { + maxRate: 1024, + adapter: async (config) => ({ + data: { maxRate: config.maxRate }, + status: 200, + statusText: 'OK', + headers: {}, + config, + }), + }); + + expect(response.data.maxRate).toBe(1024); + }); + + it('accepts tuple maxRate config [upload, download]', async () => { + const response = await axios.get('http://example.com/rate', { + maxRate: [2048, 4096], + adapter: async (config) => ({ + data: { maxRate: config.maxRate }, + status: 200, + statusText: 'OK', + headers: {}, + config, + }), + }); + + expect(response.data.maxRate).toEqual([2048, 4096]); + }); + + it('merges instance and request maxRate values', async () => { + const client = axios.create({ + maxRate: [1000, 2000], + }); + + const response = await client.get('http://example.com/rate', { + maxRate: [3000, 4000], + adapter: async (config) => ({ + data: { maxRate: config.maxRate }, + status: 200, + statusText: 'OK', + headers: {}, + config, + }), + }); + + expect(response.data.maxRate).toEqual([3000, 4000]); + }); + + it('supports maxRate in node transport flow without errors', async () => { + const { transport, getCapturedOptions } = createTransportCapture(); + + const response = await axios.get('http://example.com/rate', { + proxy: false, + maxRate: [1500, 2500], + transport, + }); + + expect(response.status).toBe(200); + expect(getCapturedOptions().method).toBe('GET'); + expect(getCapturedOptions().path).toBe('/rate'); + }); +}); diff --git a/tests/smoke/esm/tests/timeout.smoke.test.js b/tests/smoke/esm/tests/timeout.smoke.test.js new file mode 100644 index 00000000..27017629 --- /dev/null +++ b/tests/smoke/esm/tests/timeout.smoke.test.js @@ -0,0 +1,116 @@ +import { describe, expect, it } from 'vitest'; + +import { EventEmitter } from 'events'; +import { PassThrough } from 'stream'; +import axios from 'axios'; + +const createTransport = (config) => { + const opts = config || {}; + + return { + request(_options, onResponse) { + const req = new EventEmitter(); + req.destroyed = false; + req._timeoutCallback = null; + + req.setTimeout = (_ms, callback) => { + req._timeoutCallback = callback; + }; + + req.write = () => true; + + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + + req.end = () => { + if (opts.triggerTimeout && req._timeoutCallback) { + req._timeoutCallback(); + return; + } + + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = { 'content-type': 'application/json' }; + res.req = req; + + onResponse(res); + res.end(opts.body === undefined ? '{"ok":true}' : opts.body); + }; + + return req; + }, + }; +}; + +describe('timeout compat (dist export only)', () => { + it('rejects with ECONNABORTED on timeout', async () => { + const err = await axios + .get('http://example.com/timeout', { + proxy: false, + timeout: 25, + transport: createTransport({ triggerTimeout: true }), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).toBe(true); + expect(err.code).toBe('ECONNABORTED'); + expect(err.message).toBe('timeout of 25ms exceeded'); + }); + + it('uses timeoutErrorMessage when provided', async () => { + const err = await axios + .get('http://example.com/timeout', { + proxy: false, + timeout: 25, + timeoutErrorMessage: 'custom timeout', + transport: createTransport({ triggerTimeout: true }), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).toBe(true); + expect(err.code).toBe('ECONNABORTED'); + expect(err.message).toBe('custom timeout'); + }); + + it('accepts timeout as a numeric string', async () => { + const err = await axios + .get('http://example.com/timeout', { + proxy: false, + timeout: '30', + transport: createTransport({ triggerTimeout: true }), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).toBe(true); + expect(err.code).toBe('ECONNABORTED'); + expect(err.message).toBe('timeout of 30ms exceeded'); + }); + + it('rejects with ERR_BAD_OPTION_VALUE when timeout is not parsable', async () => { + const err = await axios + .get('http://example.com/timeout', { + proxy: false, + timeout: { invalid: true }, + transport: createTransport(), + }) + .catch((e) => e); + + expect(axios.isAxiosError(err)).toBe(true); + expect(err.code).toBe('ERR_BAD_OPTION_VALUE'); + expect(err.message).toBe('error trying to parse `config.timeout` to int'); + }); + + it('does not time out when timeout is 0', async () => { + const response = await axios.get('http://example.com/no-timeout', { + proxy: false, + timeout: 0, + transport: createTransport({ body: '{"ok":true}' }), + }); + + expect(response.status).toBe(200); + expect(response.data).toEqual({ ok: true }); + }); +}); diff --git a/tests/smoke/esm/tests/urlencode.smoke.test.js b/tests/smoke/esm/tests/urlencode.smoke.test.js new file mode 100644 index 00000000..1e689254 --- /dev/null +++ b/tests/smoke/esm/tests/urlencode.smoke.test.js @@ -0,0 +1,146 @@ +import { describe, expect, it } from 'vitest'; + +import { EventEmitter } from 'events'; +import { PassThrough } from 'stream'; +import axios from 'axios'; + +const createEchoTransport = () => { + let capturedOptions; + + const transport = { + request(options, onResponse) { + capturedOptions = options; + const chunks = []; + + const req = new EventEmitter(); + req.destroyed = false; + req.setTimeout = () => {}; + req.destroy = () => { + req.destroyed = true; + }; + req.close = req.destroy; + req.write = (chunk) => { + chunks.push(Buffer.from(chunk)); + return true; + }; + req.end = (chunk) => { + if (chunk) { + chunks.push(Buffer.from(chunk)); + } + + const res = new PassThrough(); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = { 'content-type': 'application/json' }; + res.req = req; + onResponse(res); + res.end( + JSON.stringify({ + path: options.path, + body: Buffer.concat(chunks).toString('utf8'), + contentType: + options.headers && + (options.headers['Content-Type'] || options.headers['content-type']), + }) + ); + }; + + return req; + }, + }; + + return { + transport, + getCapturedOptions: () => capturedOptions, + }; +}; + +describe('urlencode compat (dist export only)', () => { + it('serializes params into request URL', async () => { + const { transport } = createEchoTransport(); + + const response = await axios.get('http://example.com/search', { + proxy: false, + transport, + params: { + q: 'axios docs', + page: 2, + }, + }); + + expect(response.data.path).toBe('/search?q=axios+docs&page=2'); + }); + + it('supports custom paramsSerializer function', async () => { + const { transport } = createEchoTransport(); + + const response = await axios.get('http://example.com/search', { + proxy: false, + transport, + params: { q: 'ignored' }, + paramsSerializer: () => 'fixed=1', + }); + + expect(response.data.path).toBe('/search?fixed=1'); + }); + + it('supports URLSearchParams payloads', async () => { + const { transport } = createEchoTransport(); + const payload = new URLSearchParams(); + payload.append('name', 'axios'); + payload.append('mode', 'compat'); + + const response = await axios.post('http://example.com/form', payload, { + proxy: false, + transport, + }); + + expect(response.data.body).toBe('name=axios&mode=compat'); + expect(response.data.contentType).toContain('application/x-www-form-urlencoded'); + }); + + it('serializes object payload when content-type is application/x-www-form-urlencoded', async () => { + const { transport } = createEchoTransport(); + + const response = await axios.post( + 'http://example.com/form', + { + name: 'axios', + mode: 'compat', + }, + { + proxy: false, + transport, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + } + ); + + expect(response.data.body).toBe('name=axios&mode=compat'); + expect(response.data.contentType).toContain('application/x-www-form-urlencoded'); + }); + + it('respects formSerializer options for index formatting', async () => { + const { transport } = createEchoTransport(); + + const response = await axios.post( + 'http://example.com/form', + { + arr: ['1', '2'], + }, + { + proxy: false, + transport, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + formSerializer: { + indexes: true, + }, + } + ); + + expect(response.data.body).toBe('arr%5B0%5D=1&arr%5B1%5D=2'); + }); +}); diff --git a/tests/smoke/esm/vitest.config.js b/tests/smoke/esm/vitest.config.js new file mode 100644 index 00000000..699fb5d6 --- /dev/null +++ b/tests/smoke/esm/vitest.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + testTimeout: 10000, + projects: [ + { + test: { + name: 'smoke', + environment: 'node', + include: ['tests/**/*.smoke.test.js'], + setupFiles: [], + }, + }, + ], + }, +}); diff --git a/tests/unit/adapters/error-details.test.js b/tests/unit/adapters/errorDetails.test.js similarity index 100% rename from tests/unit/adapters/error-details.test.js rename to tests/unit/adapters/errorDetails.test.js diff --git a/tests/unit/adapters/fetch.test.js b/tests/unit/adapters/fetch.test.js index 08a620c2..d6de2ff3 100644 --- a/tests/unit/adapters/fetch.test.js +++ b/tests/unit/adapters/fetch.test.js @@ -1,9 +1,8 @@ -import { afterEach, describe, it, vi } from 'vitest'; +import { describe, it, vi } from 'vitest'; import assert from 'assert'; import { startHTTPServer, stopHTTPServer, - LOCAL_SERVER_URL, setTimeoutAsync, makeReadableStream, generateReadable, @@ -15,6 +14,9 @@ import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill.js'; import util from 'util'; import NodeFormData from 'form-data'; +const SERVER_PORT = 8010; +const LOCAL_SERVER_URL = `http://localhost:${SERVER_PORT}`; + const pipelineAsync = util.promisify(stream.pipeline); const fetchAxios = axios.create({ @@ -22,69 +24,85 @@ const fetchAxios = axios.create({ adapter: 'fetch', }); -let server; - describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => { - afterEach(async () => { - await stopHTTPServer(server); - - server = null; - }); - describe('responses', () => { it('should support text response type', async () => { const originalData = 'my data'; - server = await startHTTPServer((req, res) => res.end(originalData)); - - const { data } = await fetchAxios.get('/', { - responseType: 'text', + const server = await startHTTPServer((req, res) => res.end(originalData), { + port: SERVER_PORT, }); - assert.deepStrictEqual(data, originalData); + try { + const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + responseType: 'text', + }); + + assert.deepStrictEqual(data, originalData); + } finally { + await stopHTTPServer(server); + } }); it('should support arraybuffer response type', async () => { const originalData = 'my data'; - server = await startHTTPServer((req, res) => res.end(originalData)); - - const { data } = await fetchAxios.get('/', { - responseType: 'arraybuffer', + const server = await startHTTPServer((req, res) => res.end(originalData), { + port: SERVER_PORT, }); - assert.deepStrictEqual( - data, - Uint8Array.from(await new TextEncoder().encode(originalData)).buffer - ); + try { + const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + responseType: 'arraybuffer', + }); + + assert.deepStrictEqual( + data, + Uint8Array.from(await new TextEncoder().encode(originalData)).buffer + ); + } finally { + await stopHTTPServer(server); + } }); it('should support blob response type', async () => { const originalData = 'my data'; - server = await startHTTPServer((req, res) => res.end(originalData)); - - const { data } = await fetchAxios.get('/', { - responseType: 'blob', + const server = await startHTTPServer((req, res) => res.end(originalData), { + port: SERVER_PORT, }); - assert.deepStrictEqual(data, new Blob([originalData])); + try { + const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + responseType: 'blob', + }); + + assert.deepStrictEqual(data, new Blob([originalData])); + } finally { + await stopHTTPServer(server); + } }); it('should support stream response type', async () => { const originalData = 'my data'; - server = await startHTTPServer((req, res) => res.end(originalData)); - - const { data } = await fetchAxios.get('/', { - responseType: 'stream', + const server = await startHTTPServer((req, res) => res.end(originalData), { + port: SERVER_PORT, }); - assert.ok(data instanceof ReadableStream, 'data is not instanceof ReadableStream'); + try { + const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + responseType: 'stream', + }); - const response = new Response(data); + assert.ok(data instanceof ReadableStream, 'data is not instanceof ReadableStream'); - assert.deepStrictEqual(await response.text(), originalData); + const response = new Response(data); + + assert.deepStrictEqual(await response.text(), originalData); + } finally { + await stopHTTPServer(server); + } }); it('should support formData response type', async () => { @@ -92,295 +110,382 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => originalData.append('x', '123'); - server = await startHTTPServer(async (req, res) => { - const response = await new Response(originalData); + const server = await startHTTPServer( + async (req, res) => { + const response = await new Response(originalData); - res.setHeader('Content-Type', response.headers.get('Content-Type')); + res.setHeader('Content-Type', response.headers.get('Content-Type')); - res.end(await response.text()); - }); - - const { data } = await fetchAxios.get('/', { - responseType: 'formdata', - }); - - assert.ok(data instanceof FormData, 'data is not instanceof FormData'); - - assert.deepStrictEqual( - Object.fromEntries(data.entries()), - Object.fromEntries(originalData.entries()) + res.end(await response.text()); + }, + { port: SERVER_PORT } ); + + try { + const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + responseType: 'formdata', + }); + + assert.ok(data instanceof FormData, 'data is not instanceof FormData'); + + assert.deepStrictEqual( + Object.fromEntries(data.entries()), + Object.fromEntries(originalData.entries()) + ); + } finally { + await stopHTTPServer(server); + } }, 5000); it('should support json response type', async () => { const originalData = { x: 'my data' }; - server = await startHTTPServer((req, res) => res.end(JSON.stringify(originalData))); - - const { data } = await fetchAxios.get('/', { - responseType: 'json', + const server = await startHTTPServer((req, res) => res.end(JSON.stringify(originalData)), { + port: SERVER_PORT, }); - assert.deepStrictEqual(data, originalData); + try { + const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + responseType: 'json', + }); + + assert.deepStrictEqual(data, originalData); + } finally { + await stopHTTPServer(server); + } }); }); describe('progress', () => { describe('upload', () => { it('should support upload progress capturing', async () => { - server = await startHTTPServer({ - rate: 100 * 1024, - }); - - let content = ''; - const count = 10; - const chunk = 'test'; - const chunkLength = Buffer.byteLength(chunk); - const contentLength = count * chunkLength; - - const readable = stream.Readable.from( - (async function* () { - let i = count; - - while (i-- > 0) { - await setTimeoutAsync(1100); - content += chunk; - yield chunk; - } - })() + const server = await startHTTPServer( + { + rate: 100 * 1024, + }, + { port: SERVER_PORT } ); - const samples = []; + try { + let content = ''; + const count = 10; + const chunk = 'test'; + const chunkLength = Buffer.byteLength(chunk); + const contentLength = count * chunkLength; - const { data } = await fetchAxios.post('/', readable, { - onUploadProgress: ({ loaded, total, progress, bytes, upload }) => { - console.log( - `Upload Progress ${loaded} from ${total} bytes (${(progress * 100).toFixed(1)}%)` - ); + const readable = stream.Readable.from( + (async function* () { + let i = count; - samples.push({ - loaded, - total, - progress, - bytes, - upload, - }); - }, - headers: { - 'Content-Length': contentLength, - }, - responseType: 'text', - }); - - await setTimeoutAsync(500); - - assert.strictEqual(data, content); - - assert.deepStrictEqual( - samples, - Array.from( - (function* () { - for (let i = 1; i <= 10; i++) { - yield { - loaded: chunkLength * i, - total: contentLength, - progress: (chunkLength * i) / contentLength, - bytes: 4, - upload: true, - }; + while (i-- > 0) { + await setTimeoutAsync(1100); + content += chunk; + yield chunk; } })() - ) - ); + ); + + const samples = []; + + const { data } = await fetchAxios.post( + `http://localhost:${server.address().port}/`, + readable, + { + onUploadProgress: ({ loaded, total, progress, bytes, upload }) => { + console.log( + `Upload Progress ${loaded} from ${total} bytes (${(progress * 100).toFixed(1)}%)` + ); + + samples.push({ + loaded, + total, + progress, + bytes, + upload, + }); + }, + headers: { + 'Content-Length': contentLength, + }, + responseType: 'text', + } + ); + + await setTimeoutAsync(500); + + assert.strictEqual(data, content); + + assert.deepStrictEqual( + samples, + Array.from( + (function* () { + for (let i = 1; i <= 10; i++) { + yield { + loaded: chunkLength * i, + total: contentLength, + progress: (chunkLength * i) / contentLength, + bytes: 4, + upload: true, + }; + } + })() + ) + ); + } finally { + await stopHTTPServer(server); + } }, 15000); it('should not fail with get method', async () => { - server = await startHTTPServer((req, res) => res.end('OK')); + const server = await startHTTPServer((req, res) => res.end('OK'), { port: SERVER_PORT }); - const { data } = await fetchAxios.get('/', { - onUploadProgress() {}, - }); + try { + const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + onUploadProgress() {}, + }); - assert.strictEqual(data, 'OK'); + assert.strictEqual(data, 'OK'); + } finally { + await stopHTTPServer(server); + } }); }); describe('download', () => { it('should support download progress capturing', async () => { - server = await startHTTPServer({ - rate: 100 * 1024, - }); - - let content = ''; - const count = 10; - const chunk = 'test'; - const chunkLength = Buffer.byteLength(chunk); - const contentLength = count * chunkLength; - - const readable = stream.Readable.from( - (async function* () { - let i = count; - - while (i-- > 0) { - await setTimeoutAsync(1100); - content += chunk; - yield chunk; - } - })() + const server = await startHTTPServer( + { + rate: 100 * 1024, + }, + { + port: SERVER_PORT, + } ); - const samples = []; + try { + let content = ''; + const count = 10; + const chunk = 'test'; + const chunkLength = Buffer.byteLength(chunk); + const contentLength = count * chunkLength; - const { data } = await fetchAxios.post('/', readable, { - onDownloadProgress: ({ loaded, total, progress, bytes, download }) => { - console.log( - `Download Progress ${loaded} from ${total} bytes (${(progress * 100).toFixed(1)}%)` - ); + const readable = stream.Readable.from( + (async function* () { + let i = count; - samples.push({ - loaded, - total, - progress, - bytes, - download, - }); - }, - headers: { - 'Content-Length': contentLength, - }, - responseType: 'text', - maxRedirects: 0, - }); - - await setTimeoutAsync(500); - - assert.strictEqual(data, content); - - assert.deepStrictEqual( - samples, - Array.from( - (function* () { - for (let i = 1; i <= 10; i++) { - yield { - loaded: chunkLength * i, - total: contentLength, - progress: (chunkLength * i) / contentLength, - bytes: 4, - download: true, - }; + while (i-- > 0) { + await setTimeoutAsync(1100); + content += chunk; + yield chunk; } })() - ) - ); + ); + + const samples = []; + + const { data } = await fetchAxios.post( + `http://localhost:${server.address().port}/`, + readable, + { + onDownloadProgress: ({ loaded, total, progress, bytes, download }) => { + console.log( + `Download Progress ${loaded} from ${total} bytes (${(progress * 100).toFixed(1)}%)` + ); + + samples.push({ + loaded, + total, + progress, + bytes, + download, + }); + }, + headers: { + 'Content-Length': contentLength, + }, + responseType: 'text', + maxRedirects: 0, + } + ); + + await setTimeoutAsync(500); + + assert.strictEqual(data, content); + + assert.deepStrictEqual( + samples, + Array.from( + (function* () { + for (let i = 1; i <= 10; i++) { + yield { + loaded: chunkLength * i, + total: contentLength, + progress: (chunkLength * i) / contentLength, + bytes: 4, + download: true, + }; + } + })() + ) + ); + } finally { + await stopHTTPServer(server); + } }, 15000); }); }); it('should support basic auth', async () => { - server = await startHTTPServer((req, res) => res.end(req.headers.authorization)); + const server = await startHTTPServer((req, res) => res.end(req.headers.authorization), { + port: SERVER_PORT, + }); - const user = 'foo'; - const headers = { Authorization: 'Bearer 1234' }; - const res = await axios.get(`http://${user}@localhost:4444/`, { headers }); + try { + const user = 'foo'; + const headers = { Authorization: 'Bearer 1234' }; + const res = await axios.get(`http://${user}@localhost:${server.address().port}/`, { + headers, + }); - const base64 = Buffer.from(`${user}:`, 'utf8').toString('base64'); - assert.equal(res.data, `Basic ${base64}`); + const base64 = Buffer.from(`${user}:`, 'utf8').toString('base64'); + assert.equal(res.data, `Basic ${base64}`); + } finally { + await stopHTTPServer(server); + } }); it('should support stream.Readable as a payload', async () => { - server = await startHTTPServer(); + const server = await startHTTPServer(async (req, res) => res.end('OK'), { port: SERVER_PORT }); - const { data } = await fetchAxios.post('/', stream.Readable.from('OK')); + try { + const { data } = await fetchAxios.post( + `http://localhost:${server.address().port}/`, + stream.Readable.from('OK') + ); - assert.strictEqual(data, 'OK'); + assert.strictEqual(data, 'OK'); + } finally { + await stopHTTPServer(server); + } }); describe('request aborting', () => { it('should be able to abort the request stream', async () => { - server = await startHTTPServer({ - rate: 100000, - useBuffering: true, - }); + const server = await startHTTPServer( + { + rate: 100000, + useBuffering: true, + }, + { port: SERVER_PORT } + ); - const controller = new AbortController(); + try { + const controller = new AbortController(); - setTimeout(() => { - controller.abort(); - }, 500); + setTimeout(() => { + controller.abort(); + }, 500); - await assert.rejects(async () => { - await fetchAxios.post('/', makeReadableStream(), { - responseType: 'stream', - signal: controller.signal, - }); - }, /CanceledError/); + await assert.rejects(async () => { + await fetchAxios.post( + `http://localhost:${server.address().port}/`, + makeReadableStream(), + { + responseType: 'stream', + signal: controller.signal, + } + ); + }, /CanceledError/); + } finally { + await stopHTTPServer(server); + } }); it('should be able to abort the response stream', async () => { - server = await startHTTPServer((req, res) => { - pipelineAsync(generateReadable(10000, 10), res).catch(() => { - // Client-side abort intentionally closes the stream early in this test. + const server = await startHTTPServer( + (req, res) => { + pipelineAsync(generateReadable(10000, 10), res).catch(() => { + // Client-side abort intentionally closes the stream early in this test. + }); + }, + { port: SERVER_PORT } + ); + + try { + const controller = new AbortController(); + + setTimeout(() => { + controller.abort(new Error('test')); + }, 800); + + const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + responseType: 'stream', + signal: controller.signal, }); - }); - const controller = new AbortController(); - - setTimeout(() => { - controller.abort(new Error('test')); - }, 800); - - const { data } = await fetchAxios.get('/', { - responseType: 'stream', - signal: controller.signal, - }); - - await assert.rejects(async () => { - await data.pipeTo(makeEchoStream()); - }, /^(AbortError|CanceledError):/); + await assert.rejects(async () => { + await data.pipeTo(makeEchoStream()); + }, /^(AbortError|CanceledError):/); + } finally { + await stopHTTPServer(server); + } }); }); it('should support a timeout', async () => { - server = await startHTTPServer(async (req, res) => { - await setTimeoutAsync(1000); - res.end('OK'); - }); + const server = await startHTTPServer( + async (req, res) => { + await setTimeoutAsync(1000); + res.end('OK'); + }, + { port: SERVER_PORT } + ); - const timeout = 500; + try { + const timeout = 500; - const ts = Date.now(); + const ts = Date.now(); - await assert.rejects(async () => { - await fetchAxios('/', { - timeout, - }); - }, /timeout/); + await assert.rejects(async () => { + await fetchAxios(`http://localhost:${server.address().port}/`, { + timeout, + }); + }, /timeout/); - const passed = Date.now() - ts; + const passed = Date.now() - ts; - assert.ok(passed >= timeout - 5, `early cancellation detected (${passed} ms)`); + assert.ok(passed >= timeout - 5, `early cancellation detected (${passed} ms)`); + } finally { + await stopHTTPServer(server); + } }); it('should combine baseURL and url', async () => { - server = await startHTTPServer(); + const server = await startHTTPServer(async (req, res) => res.end('OK'), { port: SERVER_PORT }); + try { + const res = await fetchAxios('/foo'); - const res = await fetchAxios('/foo'); - - assert.equal(res.config.baseURL, LOCAL_SERVER_URL); - assert.equal(res.config.url, '/foo'); + assert.equal(res.config.baseURL, LOCAL_SERVER_URL); + assert.equal(res.config.url, '/foo'); + } finally { + await stopHTTPServer(server); + } }); it('should support params', async () => { - server = await startHTTPServer((req, res) => res.end(req.url)); + const server = await startHTTPServer((req, res) => res.end(req.url), { port: SERVER_PORT }); + try { + const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/?test=1`, { + params: { + foo: 1, + bar: 2, + }, + }); - const { data } = await fetchAxios.get('/?test=1', { - params: { - foo: 1, - bar: 2, - }, - }); - - assert.strictEqual(data, '/?test=1&foo=1&bar=2'); + assert.strictEqual(data, '/?test=1&foo=1&bar=2'); + } finally { + await stopHTTPServer(server); + } }); it('should handle fetch failed error as an AxiosError with ERR_NETWORK code', async () => { @@ -394,16 +499,23 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => }); it('should get response headers', async () => { - server = await startHTTPServer((req, res) => { - res.setHeader('foo', 'bar'); - res.end(req.url); - }); + const server = await startHTTPServer( + (req, res) => { + res.setHeader('foo', 'bar'); + res.end(req.url); + }, + { port: SERVER_PORT } + ); - const { headers } = await fetchAxios.get('/', { - responseType: 'stream', - }); + try { + const { headers } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + responseType: 'stream', + }); - assert.strictEqual(headers.get('foo'), 'bar'); + assert.strictEqual(headers.get('foo'), 'bar'); + } finally { + await stopHTTPServer(server); + } }); describe('fetch adapter - Content-Type handling', () => { @@ -411,13 +523,20 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => const form = new NodeFormData(); form.append('foo', 'bar'); - server = await startHTTPServer((req, res) => { - const contentType = req.headers['content-type']; - assert.match(contentType, /^multipart\/form-data; boundary=/i); - res.end('OK'); - }); + const server = await startHTTPServer( + (req, res) => { + const contentType = req.headers['content-type']; + assert.match(contentType, /^multipart\/form-data; boundary=/i); + res.end('OK'); + }, + { port: SERVER_PORT } + ); - await fetchAxios.post('/form', form); + try { + await fetchAxios.post(`http://localhost:${server.address().port}/form`, form); + } finally { + await stopHTTPServer(server); + } }); }); @@ -490,15 +609,19 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => }); it('should fallback to the global on undefined env value', async () => { - server = await startHTTPServer((req, res) => res.end('OK')); + const server = await startHTTPServer((req, res) => res.end('OK'), { port: SERVER_PORT }); - const { data } = await fetchAxios.get('/', { - env: { - fetch: undefined, - }, - }); + try { + const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + env: { + fetch: undefined, + }, + }); - assert.strictEqual(data, 'OK'); + assert.strictEqual(data, 'OK'); + } finally { + await stopHTTPServer(server); + } }); it('should use current global fetch when env fetch is not specified', async () => { @@ -513,10 +636,10 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => }; }); - try { - server = await startHTTPServer((req, res) => res.end('OK')); + const server = await startHTTPServer((req, res) => res.end('OK'), { port: SERVER_PORT }); - const { data } = await fetchAxios.get('/', { + try { + const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { env: { fetch: undefined, }, @@ -525,6 +648,7 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => assert.strictEqual(data, 'global'); } finally { vi.stubGlobal('fetch', globalFetch); + await stopHTTPServer(server); } }); }); diff --git a/tests/unit/adapters/http.test.js b/tests/unit/adapters/http.test.js index ccb3485b..270c52e4 100644 --- a/tests/unit/adapters/http.test.js +++ b/tests/unit/adapters/http.test.js @@ -31,6 +31,11 @@ import bodyParser from 'body-parser'; import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill.js'; import { lookup } from 'dns'; +const OPEN_WEB_PORT = 80; +const SERVER_PORT = 8020; +const PROXY_PORT = 8030; +const ALTERNATE_SERVER_PORT = 8040; + describe('supports http with nodejs', () => { const adaptersTestsDir = path.join(process.cwd(), 'tests/unit/adapters'); const thisTestFilePath = path.join(adaptersTestsDir, 'http.test.js'); @@ -59,7 +64,7 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(data)); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -82,7 +87,7 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(data)); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -102,7 +107,7 @@ describe('supports http with nodejs', () => { res.end(); }, 1000); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -128,7 +133,7 @@ describe('supports http with nodejs', () => { res.end(); }, 1000); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -154,7 +159,7 @@ describe('supports http with nodejs', () => { res.end(); }, 1000); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -180,7 +185,7 @@ describe('supports http with nodejs', () => { res.end(); }, 1000); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -212,7 +217,7 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(data)); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -237,7 +242,7 @@ describe('supports http with nodejs', () => { const jsonBuffer = Buffer.from(JSON.stringify(data)); res.end(Buffer.concat([bomBuffer, jsonBuffer])); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -261,7 +266,7 @@ describe('supports http with nodejs', () => { res.end(expectedResponse); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -283,7 +288,7 @@ describe('supports http with nodejs', () => { res.statusCode = 302; res.end(); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -311,7 +316,7 @@ describe('supports http with nodejs', () => { res.end(); i++; }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -333,7 +338,7 @@ describe('supports http with nodejs', () => { res.statusCode = 302; res.end(); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -362,12 +367,12 @@ describe('supports http with nodejs', () => { (req, res) => { requestCount += 1; if (requestCount <= totalRedirectCount) { - res.setHeader('Location', 'http://localhost:8080'); + res.setHeader('Location', `http://localhost:${SERVER_PORT}`); res.writeHead(302); } res.end(); }, - { port: 8080 } + { port: SERVER_PORT } ); const proxy = await startHTTPServer( @@ -392,13 +397,13 @@ describe('supports http with nodejs', () => { res.end(); }); }, - { port: 4000 } + { port: PROXY_PORT } ); await axios.get(`http://localhost:${server.address().port}/`, { proxy: { host: 'localhost', - port: 4000, + port: PROXY_PORT, }, maxRedirects: totalRedirectCount, beforeRedirect: (options) => { @@ -419,7 +424,7 @@ describe('supports http with nodejs', () => { res.statusCode = 400; res.end(); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -455,7 +460,7 @@ describe('supports http with nodejs', () => { (req, res) => { res.end(); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -496,7 +501,7 @@ describe('supports http with nodejs', () => { res.end(); } }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -532,7 +537,7 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Encoding', 'gzip'); res.end(zipped); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -552,7 +557,7 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Encoding', 'gzip'); res.end('invalid response'); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -584,7 +589,7 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Encoding', 'gzip'); res.end(zipped); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -661,10 +666,13 @@ describe('supports http with nodejs', () => { describe(`${typeName} decompression`, () => { it('should support decompression', async () => { - const server = await startHTTPServer(async (req, res) => { - res.setHeader('Content-Encoding', type); - res.end(await zipped); - }); + const server = await startHTTPServer( + async (req, res) => { + res.setHeader('Content-Encoding', type); + res.end(await zipped); + }, + { port: SERVER_PORT } + ); try { const { data } = await axios.get(`http://localhost:${server.address().port}`); @@ -675,11 +683,14 @@ describe('supports http with nodejs', () => { }); it(`should not fail if response content-length header is missing (${type})`, async () => { - const server = await startHTTPServer(async (req, res) => { - res.setHeader('Content-Encoding', type); - res.removeHeader('Content-Length'); - res.end(await zipped); - }); + const server = await startHTTPServer( + async (req, res) => { + res.setHeader('Content-Encoding', type); + res.removeHeader('Content-Length'); + res.end(await zipped); + }, + { port: SERVER_PORT } + ); try { const { data } = await axios.get(`http://localhost:${server.address().port}`); @@ -690,13 +701,16 @@ describe('supports http with nodejs', () => { }); it('should not fail with chunked responses (without Content-Length header)', async () => { - const server = await startHTTPServer(async (req, res) => { - res.setHeader('Content-Encoding', type); - res.setHeader('Transfer-Encoding', 'chunked'); - res.removeHeader('Content-Length'); - res.write(await zipped); - res.end(); - }); + const server = await startHTTPServer( + async (req, res) => { + res.setHeader('Content-Encoding', type); + res.setHeader('Transfer-Encoding', 'chunked'); + res.removeHeader('Content-Length'); + res.write(await zipped); + res.end(); + }, + { port: SERVER_PORT } + ); try { const { data } = await axios.get(`http://localhost:${server.address().port}`); @@ -707,11 +721,14 @@ describe('supports http with nodejs', () => { }); it('should not fail with an empty response without content-length header (Z_BUF_ERROR)', async () => { - const server = await startHTTPServer((req, res) => { - res.setHeader('Content-Encoding', type); - res.removeHeader('Content-Length'); - res.end(); - }); + const server = await startHTTPServer( + (req, res) => { + res.setHeader('Content-Encoding', type); + res.removeHeader('Content-Length'); + res.end(); + }, + { port: SERVER_PORT } + ); try { const { data } = await axios.get(`http://localhost:${server.address().port}`); @@ -722,10 +739,13 @@ describe('supports http with nodejs', () => { }); it('should not fail with an empty response with content-length header (Z_BUF_ERROR)', async () => { - const server = await startHTTPServer((req, res) => { - res.setHeader('Content-Encoding', type); - res.end(); - }); + const server = await startHTTPServer( + (req, res) => { + res.setHeader('Content-Encoding', type); + res.end(); + }, + { port: SERVER_PORT } + ); try { await axios.get(`http://localhost:${server.address().port}`); @@ -746,7 +766,7 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.end(str); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -762,7 +782,7 @@ describe('supports http with nodejs', () => { (req, res) => { res.end(req.headers.authorization); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -783,7 +803,7 @@ describe('supports http with nodejs', () => { (req, res) => { res.end(req.headers.authorization); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -805,7 +825,7 @@ describe('supports http with nodejs', () => { (req, res) => { res.end(req.headers['user-agent']); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -824,7 +844,7 @@ describe('supports http with nodejs', () => { (req, res) => { res.end(req.headers['user-agent']); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -842,7 +862,7 @@ describe('supports http with nodejs', () => { assert.strictEqual(req.headers['content-length'], '42'); res.end(); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -859,7 +879,7 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.end(Array(5000).join('#')); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -891,7 +911,7 @@ describe('supports http with nodejs', () => { res.statusCode = 302; res.end(); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -916,7 +936,7 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.end(); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -953,7 +973,7 @@ describe('supports http with nodejs', () => { res.end('OK'); }); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -967,7 +987,7 @@ describe('supports http with nodejs', () => { }); it('should display error while parsing params', async () => { - const server = await startHTTPServer(() => {}, { port: 8080 }); + const server = await startHTTPServer(() => {}, { port: SERVER_PORT }); try { await assert.rejects( @@ -1044,7 +1064,7 @@ describe('supports http with nodejs', () => { (req, res) => { req.pipe(res); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -1077,7 +1097,7 @@ describe('supports http with nodejs', () => { }); it('should pass errors for a failed stream', async () => { - const server = await startHTTPServer(() => {}, { port: 8080 }); + const server = await startHTTPServer(() => {}, { port: SERVER_PORT }); const notExistPath = path.join(adaptersTestsDir, 'does_not_exist'); try { @@ -1148,7 +1168,7 @@ describe('supports http with nodejs', () => { assert.strictEqual(req.headers['content-length'], buf.length.toString()); req.pipe(res); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -1182,7 +1202,7 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.end('12345'); }, - { port: 8080 } + { port: SERVER_PORT } ); const proxy = await startHTTPServer( @@ -1207,7 +1227,7 @@ describe('supports http with nodejs', () => { }); }); }, - { port: 0 } + { port: PROXY_PORT } ); try { @@ -1251,9 +1271,9 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.end('12345'); }, - { port: 8080 } + { port: SERVER_PORT } ) - .listen(8080, () => resolve(httpsServer)); + .listen(SERVER_PORT, () => resolve(httpsServer)); httpsServer.on('error', reject); }); @@ -1290,9 +1310,9 @@ describe('supports http with nodejs', () => { response.end(); }); }, - { port: 8081 } + { port: PROXY_PORT } ) - .listen(8081, () => resolve(httpsProxy)); + .listen(PROXY_PORT, () => resolve(httpsProxy)); httpsProxy.on('error', reject); }); @@ -1324,7 +1344,7 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.end('123456789'); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -1355,7 +1375,7 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.end('4567'); }, - { port: 8080 } + { port: SERVER_PORT } ); const proxy = await startHTTPServer( @@ -1380,7 +1400,7 @@ describe('supports http with nodejs', () => { }); }); }, - { port: 8081 } + { port: PROXY_PORT } ); const proxyUrl = `http://localhost:${proxy.address().port}/`; @@ -1458,9 +1478,9 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.end('12345'); }, - { port: 8080 } + { port: SERVER_PORT } ) - .listen(8080, () => resolve(httpsServer)); + .listen(SERVER_PORT, () => resolve(httpsServer)); httpsServer.on('error', reject); }); @@ -1497,9 +1517,9 @@ describe('supports http with nodejs', () => { response.end(); }); }, - { port: 8081 } + { port: PROXY_PORT } ) - .listen(8081, () => resolve(httpsProxy)); + .listen(PROXY_PORT, () => resolve(httpsProxy)); httpsProxy.on('error', reject); }); @@ -1561,7 +1581,7 @@ describe('supports http with nodejs', () => { res.statusCode = 302; res.end(); }, - { port: 8080 } + { port: SERVER_PORT } ); const proxy = await startHTTPServer( @@ -1597,7 +1617,7 @@ describe('supports http with nodejs', () => { }); }); }, - { port: 8081 } + { port: PROXY_PORT } ); const proxyUrl = `http://localhost:${proxy.address().port}`; @@ -1651,7 +1671,7 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.end('4567'); }, - { port: 8080 } + { port: SERVER_PORT } ); const proxy = await startHTTPServer( @@ -1676,7 +1696,7 @@ describe('supports http with nodejs', () => { }); }); }, - { port: 8081 } + { port: PROXY_PORT } ); const noProxyValue = 'foo.com, localhost,bar.net , , quix.co'; @@ -1730,7 +1750,7 @@ describe('supports http with nodejs', () => { res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.end('4567'); }, - { port: 8080 } + { port: SERVER_PORT } ); const proxy = await startHTTPServer( @@ -1755,7 +1775,7 @@ describe('supports http with nodejs', () => { }); }); }, - { port: 8081 } + { port: PROXY_PORT } ); const noProxyValue = 'foo.com, ,bar.net , quix.co'; @@ -1803,7 +1823,7 @@ describe('supports http with nodejs', () => { (req, res) => { res.end(); }, - { port: 8080 } + { port: SERVER_PORT } ); const proxy = await startHTTPServer( @@ -1825,7 +1845,7 @@ describe('supports http with nodejs', () => { }); }); }, - { port: 8081 } + { port: PROXY_PORT } ); try { @@ -1858,7 +1878,7 @@ describe('supports http with nodejs', () => { (req, res) => { res.end(); }, - { port: 8080 } + { port: SERVER_PORT } ); const proxy = await startHTTPServer( @@ -1880,7 +1900,7 @@ describe('supports http with nodejs', () => { }); }); }, - { port: 8081 } + { port: PROXY_PORT } ); const proxyUrl = `http://user:pass@localhost:${proxy.address().port}/`; @@ -1932,7 +1952,7 @@ describe('supports http with nodejs', () => { const proxy = { protocol: 'http:', host: 'hostname.abc.xyz', - port: 3300, + port: PROXY_PORT, auth: { username: '', password: '', @@ -1954,23 +1974,48 @@ describe('supports http with nodejs', () => { const testCases = [ { description: 'hostname and trailing colon in protocol', - proxyConfig: { hostname: '127.0.0.1', protocol: 'http:', port: 80 }, - expectedOptions: { host: '127.0.0.1', protocol: 'http:', port: 80, path: destination }, + proxyConfig: { hostname: '127.0.0.1', protocol: 'http:', port: OPEN_WEB_PORT }, + expectedOptions: { + host: '127.0.0.1', + protocol: 'http:', + port: OPEN_WEB_PORT, + path: destination, + }, }, { description: 'hostname and no trailing colon in protocol', - proxyConfig: { hostname: '127.0.0.1', protocol: 'http', port: 80 }, - expectedOptions: { host: '127.0.0.1', protocol: 'http:', port: 80, path: destination }, + proxyConfig: { hostname: '127.0.0.1', protocol: 'http', port: OPEN_WEB_PORT }, + expectedOptions: { + host: '127.0.0.1', + protocol: 'http:', + port: OPEN_WEB_PORT, + path: destination, + }, }, { description: 'both hostname and host -> hostname takes precedence', - proxyConfig: { hostname: '127.0.0.1', host: '0.0.0.0', protocol: 'http', port: 80 }, - expectedOptions: { host: '127.0.0.1', protocol: 'http:', port: 80, path: destination }, + proxyConfig: { + hostname: '127.0.0.1', + host: '0.0.0.0', + protocol: 'http', + port: OPEN_WEB_PORT, + }, + expectedOptions: { + host: '127.0.0.1', + protocol: 'http:', + port: OPEN_WEB_PORT, + path: destination, + }, }, { description: 'only host and https protocol', - proxyConfig: { host: '0.0.0.0', protocol: 'https', port: 80 }, - expectedOptions: { host: '0.0.0.0', protocol: 'https:', port: 80, path: destination }, + proxyConfig: { host: '0.0.0.0', protocol: 'https', port: OPEN_WEB_PORT }, + expectedOptions: { + host: '0.0.0.0', + protocol: 'https:', + port: OPEN_WEB_PORT, + path: destination, + }, }, ]; @@ -1994,7 +2039,7 @@ describe('supports http with nodejs', () => { // Call cancel() when the request has been sent but no response received. source.cancel('Operation has been canceled.'); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -2024,7 +2069,7 @@ describe('supports http with nodejs', () => { (req, res) => { res.end(); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -2046,7 +2091,7 @@ describe('supports http with nodejs', () => { res.end(); }, 1000); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -2072,9 +2117,9 @@ describe('supports http with nodejs', () => { res.end(); }, 1000); }, - { port: 8080 } + { port: SERVER_PORT } ) - .listen(8080, () => resolve(httpsServer)); + .listen(SERVER_PORT, () => resolve(httpsServer)); httpsServer.on('error', reject); }); @@ -2120,7 +2165,7 @@ describe('supports http with nodejs', () => { assert.equal(req.headers['user-agent'], `axios/${axios.VERSION}`); res.end(); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -2137,7 +2182,7 @@ describe('supports http with nodejs', () => { assert.equal('User-Agent' in req.headers, false); res.end(); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -2165,7 +2210,7 @@ describe('supports http with nodejs', () => { res.destroy(); }, 200); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -2190,7 +2235,7 @@ describe('supports http with nodejs', () => { (req, res) => { res.end('ok'); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -2259,7 +2304,7 @@ describe('supports http with nodejs', () => { ); }); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -2297,7 +2342,7 @@ describe('supports http with nodejs', () => { }) ); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -2355,7 +2400,7 @@ describe('supports http with nodejs', () => { const expressServer = app.listen(0, () => resolve(expressServer)); expressServer.on('error', reject); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -2391,7 +2436,7 @@ describe('supports http with nodejs', () => { async (req, res) => { res.end(await getStream(req)); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -2433,7 +2478,7 @@ describe('supports http with nodejs', () => { const expressServer = app.listen(0, () => resolve(expressServer)); expressServer.on('error', reject); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -2475,7 +2520,7 @@ describe('supports http with nodejs', () => { (req, res) => { req.pipe(res); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -2541,7 +2586,7 @@ describe('supports http with nodejs', () => { { rate: 100 * 1024, }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -2610,7 +2655,7 @@ describe('supports http with nodejs', () => { { rate: 100 * 1024, }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -2783,10 +2828,13 @@ describe('supports http with nodejs', () => { describe('request aborting', () => { it('should be able to abort the response stream', async () => { - const server = await startHTTPServer({ - rate: 100000, - useBuffering: true, - }); + const server = await startHTTPServer( + { + rate: 100000, + useBuffering: true, + }, + { port: SERVER_PORT } + ); try { const buf = Buffer.alloc(1024 * 1024); @@ -2832,7 +2880,7 @@ describe('supports http with nodejs', () => { }); it('should support function as paramsSerializer value', async () => { - const server = await startHTTPServer((req, res) => res.end(req.url)); + const server = await startHTTPServer((req, res) => res.end(req.url), { port: SERVER_PORT }); try { const { data } = await axios.post(`http://localhost:${server.address().port}`, 'test', { @@ -2989,7 +3037,7 @@ describe('supports http with nodejs', () => { }) ); }, - { port: 8080 } + { port: SERVER_PORT } ); try { @@ -3043,7 +3091,7 @@ describe('supports http with nodejs', () => { }, { useHTTP2: true, - port: 8080, + port: SERVER_PORT, } ); @@ -3060,7 +3108,7 @@ describe('supports http with nodejs', () => { it('should support request payload', async () => { const server = await startHTTPServer(null, { useHTTP2: true, - port: 8080, + port: SERVER_PORT, }); try { @@ -3092,7 +3140,7 @@ describe('supports http with nodejs', () => { }, { useHTTP2: true, - port: 8080, + port: SERVER_PORT, } ); @@ -3134,7 +3182,7 @@ describe('supports http with nodejs', () => { }, { useHTTP2: true, - port: 8080, + port: SERVER_PORT, } ); @@ -3165,7 +3213,7 @@ describe('supports http with nodejs', () => { }, { useHTTP2: true, - port: 8080, + port: SERVER_PORT, } ); @@ -3210,7 +3258,7 @@ describe('supports http with nodejs', () => { }, { useHTTP2: true, - port: 8080, + port: SERVER_PORT, } ); @@ -3251,7 +3299,7 @@ describe('supports http with nodejs', () => { }, { useHTTP2: true, - port: 8080, + port: SERVER_PORT, } ); @@ -3302,7 +3350,7 @@ describe('supports http with nodejs', () => { }, { useHTTP2: true, - port: 8080, + port: SERVER_PORT, } ); @@ -3339,7 +3387,7 @@ describe('supports http with nodejs', () => { }, { useHTTP2: true, - port: 8080, + port: SERVER_PORT, } ); @@ -3351,7 +3399,7 @@ describe('supports http with nodejs', () => { }, { useHTTP2: true, - port: 8081, + port: ALTERNATE_SERVER_PORT, } ); @@ -3389,7 +3437,7 @@ describe('supports http with nodejs', () => { }, { useHTTP2: true, - port: 8080, + port: SERVER_PORT, } ); @@ -3428,7 +3476,7 @@ describe('supports http with nodejs', () => { }, { useHTTP2: true, - port: 8080, + port: SERVER_PORT, } ); @@ -3470,7 +3518,7 @@ describe('supports http with nodejs', () => { }, { useHTTP2: true, - port: 8080, + port: SERVER_PORT, } ); @@ -3542,7 +3590,7 @@ describe('supports http with nodejs', () => { res.end('ok'); }, - { port: 8080 } + { port: SERVER_PORT } ); try { diff --git a/tests/unit/api.test.js b/tests/unit/api.test.js new file mode 100644 index 00000000..992b307b --- /dev/null +++ b/tests/unit/api.test.js @@ -0,0 +1,97 @@ +import { describe, it } from 'vitest'; +import assert from 'assert'; +import axios from '../../index.js'; + +describe('static api', () => { + it('should have request method helpers', () => { + assert.strictEqual(typeof axios.request, 'function'); + assert.strictEqual(typeof axios.get, 'function'); + assert.strictEqual(typeof axios.head, 'function'); + assert.strictEqual(typeof axios.options, 'function'); + assert.strictEqual(typeof axios.delete, 'function'); + assert.strictEqual(typeof axios.post, 'function'); + assert.strictEqual(typeof axios.put, 'function'); + assert.strictEqual(typeof axios.patch, 'function'); + }); + + it('should have promise method helpers', async () => { + const promise = axios.request({ + url: '/test', + adapter: (config) => + Promise.resolve({ + data: null, + status: 200, + statusText: 'OK', + headers: {}, + config, + request: {}, + }), + }); + + assert.strictEqual(typeof promise.then, 'function'); + assert.strictEqual(typeof promise.catch, 'function'); + + await promise; + }); + + it('should have defaults', () => { + assert.strictEqual(typeof axios.defaults, 'object'); + assert.strictEqual(typeof axios.defaults.headers, 'object'); + }); + + it('should have interceptors', () => { + assert.strictEqual(typeof axios.interceptors.request, 'object'); + assert.strictEqual(typeof axios.interceptors.response, 'object'); + }); + + it('should have all/spread helpers', () => { + assert.strictEqual(typeof axios.all, 'function'); + assert.strictEqual(typeof axios.spread, 'function'); + }); + + it('should have factory method', () => { + assert.strictEqual(typeof axios.create, 'function'); + }); + + it('should have CanceledError, CancelToken, and isCancel properties', () => { + assert.strictEqual(typeof axios.Cancel, 'function'); + assert.strictEqual(typeof axios.CancelToken, 'function'); + assert.strictEqual(typeof axios.isCancel, 'function'); + }); + + it('should have getUri method', () => { + assert.strictEqual(typeof axios.getUri, 'function'); + }); + + it('should have isAxiosError properties', () => { + assert.strictEqual(typeof axios.isAxiosError, 'function'); + }); + + it('should have mergeConfig properties', () => { + assert.strictEqual(typeof axios.mergeConfig, 'function'); + }); + + it('should have getAdapter properties', () => { + assert.strictEqual(typeof axios.getAdapter, 'function'); + }); +}); + +describe('instance api', () => { + const instance = axios.create(); + + it('should have request methods', () => { + assert.strictEqual(typeof instance.request, 'function'); + assert.strictEqual(typeof instance.get, 'function'); + assert.strictEqual(typeof instance.options, 'function'); + assert.strictEqual(typeof instance.head, 'function'); + assert.strictEqual(typeof instance.delete, 'function'); + assert.strictEqual(typeof instance.post, 'function'); + assert.strictEqual(typeof instance.put, 'function'); + assert.strictEqual(typeof instance.patch, 'function'); + }); + + it('should have interceptors', () => { + assert.strictEqual(typeof instance.interceptors.request, 'object'); + assert.strictEqual(typeof instance.interceptors.response, 'object'); + }); +}); diff --git a/tests/unit/cancel/canceledError.test.js b/tests/unit/cancel/canceledError.test.js new file mode 100644 index 00000000..c8033542 --- /dev/null +++ b/tests/unit/cancel/canceledError.test.js @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest'; +import { isNativeError } from 'node:util/types'; +import CanceledError from '../../../lib/cancel/CanceledError.js'; + +describe('cancel::CanceledError', () => { + describe('toString', () => { + it('returns the default message when message is not specified', () => { + const cancel = new CanceledError(); + + expect(cancel.toString()).toBe('CanceledError: canceled'); + }); + + it('returns the provided message when message is specified', () => { + const cancel = new CanceledError('Operation has been canceled.'); + + expect(cancel.toString()).toBe('CanceledError: Operation has been canceled.'); + }); + }); + + it('is recognized as a native error by Node util/types', () => { + expect(isNativeError(new CanceledError('My Canceled Error'))).toBe(true); + }); +}); diff --git a/tests/unit/cancel/isCancel.test.js b/tests/unit/cancel/isCancel.test.js new file mode 100644 index 00000000..87c388ee --- /dev/null +++ b/tests/unit/cancel/isCancel.test.js @@ -0,0 +1,13 @@ +import { describe, it, expect } from 'vitest'; +import isCancel from '../../../lib/cancel/isCancel.js'; +import CanceledError from '../../../lib/cancel/CanceledError.js'; + +describe('cancel::isCancel', () => { + it('returns true when value is a CanceledError', () => { + expect(isCancel(new CanceledError())).toBe(true); + }); + + it('returns false when value is not canceled', () => { + expect(isCancel({ foo: 'bar' })).toBe(false); + }); +}); diff --git a/tests/unit/core/AxiosError.test.js b/tests/unit/core/AxiosError.test.js new file mode 100644 index 00000000..54301512 --- /dev/null +++ b/tests/unit/core/AxiosError.test.js @@ -0,0 +1,102 @@ +import { describe, it, expect } from 'vitest'; +import { isNativeError } from 'node:util/types'; +import AxiosError from '../../../lib/core/AxiosError.js'; + +describe('core::AxiosError', () => { + it('creates an error with message, config, code, request, response, stack and isAxiosError', () => { + const request = { path: '/foo' }; + const response = { status: 200, data: { foo: 'bar' } }; + const error = new AxiosError('Boom!', 'ESOMETHING', { foo: 'bar' }, request, response); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toBe('Boom!'); + expect(error.config).toEqual({ foo: 'bar' }); + expect(error.code).toBe('ESOMETHING'); + expect(error.request).toBe(request); + expect(error.response).toBe(response); + expect(error.isAxiosError).toBe(true); + expect(error.stack).toBeDefined(); + }); + + it('serializes to JSON safely', () => { + // request/response are intentionally omitted from the serialized shape + // to avoid circular-reference problems. + const request = { path: '/foo' }; + const response = { status: 200, data: { foo: 'bar' } }; + const error = new AxiosError('Boom!', 'ESOMETHING', { foo: 'bar' }, request, response); + const json = error.toJSON(); + + expect(json.message).toBe('Boom!'); + expect(json.config).toEqual({ foo: 'bar' }); + expect(json.code).toBe('ESOMETHING'); + expect(json.status).toBe(200); + expect(json.request).toBeUndefined(); + expect(json.response).toBeUndefined(); + }); + + describe('AxiosError.from', () => { + it('adds config, code, request and response to the wrapped error', () => { + const error = new Error('Boom!'); + const request = { path: '/foo' }; + const response = { status: 200, data: { foo: 'bar' } }; + + const axiosError = AxiosError.from(error, 'ESOMETHING', { foo: 'bar' }, request, response); + + expect(axiosError.config).toEqual({ foo: 'bar' }); + expect(axiosError.code).toBe('ESOMETHING'); + expect(axiosError.request).toBe(request); + expect(axiosError.response).toBe(response); + expect(axiosError.isAxiosError).toBe(true); + }); + + it('returns an AxiosError instance', () => { + const axiosError = AxiosError.from(new Error('Boom!'), 'ESOMETHING', { foo: 'bar' }); + + expect(axiosError).toBeInstanceOf(AxiosError); + }); + + it('preserves status from the original error when response is not provided', () => { + const error = new Error('Network Error'); + error.status = 404; + + const axiosError = AxiosError.from(error, 'ERR_NETWORK', { foo: 'bar' }); + + expect(axiosError.status).toBe(404); + }); + + it('prefers response.status over error.status when response is provided', () => { + const error = new Error('Error'); + error.status = 500; + const response = { status: 404 }; + + const axiosError = AxiosError.from(error, 'ERR_BAD_REQUEST', {}, null, response); + + expect(axiosError.status).toBe(404); + }); + }); + + it('is recognized as a native error by Node util/types', () => { + expect(isNativeError(new AxiosError('My Axios Error'))).toBe(true); + }); + + it('supports static error-code properties', () => { + const error = new AxiosError('My Axios Error', AxiosError.ECONNABORTED); + + expect(error.code).toBe(AxiosError.ECONNABORTED); + }); + + it('sets status when response is passed to constructor', () => { + const error = new AxiosError('test', 'foo', {}, {}, { status: 400 }); + + expect(error.status).toBe(400); + }); + + it('keeps message enumerable for backward compatibility', () => { + const error = new AxiosError('Test error message', 'ERR_TEST', { foo: 'bar' }); + + expect(Object.keys(error)).toContain('message'); + expect(Object.entries(error).find(([key]) => key === 'message')?.[1]).toBe('Test error message'); + expect({ ...error }.message).toBe('Test error message'); + expect(Object.getOwnPropertyDescriptor(error, 'message')?.enumerable).toBe(true); + }); +}); diff --git a/tests/unit/core/buildFullPath.test.js b/tests/unit/core/buildFullPath.test.js new file mode 100644 index 00000000..4191b443 --- /dev/null +++ b/tests/unit/core/buildFullPath.test.js @@ -0,0 +1,34 @@ +import { describe, it, expect } from 'vitest'; +import buildFullPath from '../../../lib/core/buildFullPath.js'; + +describe('core::buildFullPath', () => { + it('combines URLs when the requested URL is relative', () => { + expect(buildFullPath('https://api.github.com', '/users')).toBe('https://api.github.com/users'); + }); + + it('does not combine URLs when the requested URL is absolute', () => { + expect(buildFullPath('https://api.github.com', 'https://api.example.com/users')).toBe( + 'https://api.example.com/users' + ); + }); + + it('combines URLs when requested URL is absolute and allowAbsoluteUrls is false', () => { + expect(buildFullPath('https://api.github.com', 'https://api.example.com/users', false)).toBe( + 'https://api.github.com/https://api.example.com/users' + ); + }); + + it('does not combine URLs when baseURL is missing and allowAbsoluteUrls is false', () => { + expect(buildFullPath(undefined, 'https://api.example.com/users', false)).toBe( + 'https://api.example.com/users' + ); + }); + + it('does not combine URLs when baseURL is not configured', () => { + expect(buildFullPath(undefined, '/users')).toBe('/users'); + }); + + it('combines URLs when baseURL and requested URL are both relative', () => { + expect(buildFullPath('/api', '/users')).toBe('/api/users'); + }); +}); diff --git a/tests/unit/core/mergeConfig.test.js b/tests/unit/core/mergeConfig.test.js new file mode 100644 index 00000000..5150a8e4 --- /dev/null +++ b/tests/unit/core/mergeConfig.test.js @@ -0,0 +1,357 @@ +import { describe, it, expect } from 'vitest'; +import defaults from '../../../lib/defaults/index.js'; +import mergeConfig from '../../../lib/core/mergeConfig.js'; +import { AxiosHeaders } from '../../../index.js'; + +describe('core::mergeConfig', () => { + it('accepts undefined for second argument', () => { + expect(mergeConfig(defaults, undefined)).toEqual(defaults); + }); + + it('accepts an object for second argument', () => { + expect(mergeConfig(defaults, {})).toEqual(defaults); + }); + + it('does not leave references', () => { + const merged = mergeConfig(defaults, {}); + + expect(merged).not.toBe(defaults); + expect(merged.headers).not.toBe(defaults.headers); + }); + + it('allows setting request options', () => { + const config = { + url: '__sample url__', + method: '__sample method__', + params: '__sample params__', + data: { foo: true }, + }; + const merged = mergeConfig(defaults, config); + + expect(merged.url).toBe(config.url); + expect(merged.method).toBe(config.method); + expect(merged.params).toBe(config.params); + expect(merged.data).toEqual(config.data); + }); + + it('does not inherit request options', () => { + const localDefaults = { + method: '__sample method__', + data: { foo: true }, + }; + const merged = mergeConfig(localDefaults, {}); + + expect(merged.method).toBeUndefined(); + expect(merged.data).toBeUndefined(); + }); + + for (const key of ['auth', 'headers', 'params', 'proxy']) { + it(`sets new config for ${key} without default`, () => { + const config1 = { [key]: undefined }; + const config2 = { [key]: { user: 'foo', pass: 'test' } }; + const expected = { [key]: { user: 'foo', pass: 'test' } }; + + expect(mergeConfig(config1, config2)).toEqual(expected); + }); + + it(`merges ${key} with defaults`, () => { + const config1 = { [key]: { user: 'foo', pass: 'bar' } }; + const config2 = { [key]: { pass: 'test' } }; + const expected = { [key]: { user: 'foo', pass: 'test' } }; + + expect(mergeConfig(config1, config2)).toEqual(expected); + }); + + it.each([false, null, 123])(`overwrites default ${key} with %p`, (value) => { + const config1 = { [key]: { user: 'foo', pass: 'test' } }; + const config2 = { [key]: value }; + const expected = { [key]: value }; + + expect(mergeConfig(config1, config2)).toEqual(expected); + }); + } + + it('allows setting other options', () => { + const merged = mergeConfig(defaults, { timeout: 123 }); + + expect(merged.timeout).toBe(123); + }); + + it('allows setting custom options', () => { + const merged = mergeConfig(defaults, { foo: 'bar' }); + + expect(merged.foo).toBe('bar'); + }); + + it('allows setting custom default options', () => { + const merged = mergeConfig({ foo: 'bar' }, {}); + + expect(merged.foo).toBe('bar'); + }); + + it('allows merging custom objects in config', () => { + const merged = mergeConfig( + { + nestedConfig: { + propertyOnDefaultConfig: true, + }, + }, + { + nestedConfig: { + propertyOnRequestConfig: true, + }, + } + ); + + expect(merged.nestedConfig.propertyOnDefaultConfig).toBe(true); + expect(merged.nestedConfig.propertyOnRequestConfig).toBe(true); + }); + + describe('headers', () => { + it('allows merging with AxiosHeaders instances', () => { + const merged = mergeConfig( + { + headers: new AxiosHeaders({ + x: 1, + y: 2, + }), + }, + { + headers: new AxiosHeaders({ + X: 1, + Y: 2, + }), + } + ); + + expect(merged.headers).toEqual({ + x: '1', + y: '2', + }); + }); + }); + + describe('valueFromConfig2Keys', () => { + const config1 = { url: '/foo', method: 'post', data: { a: 3 } }; + + it('skips if config2 does not define the key', () => { + expect(mergeConfig(config1, {})).toEqual({}); + }); + + it('clones config2 when it is a plain object', () => { + const data = { a: 1, b: 2 }; + const merged = mergeConfig(config1, { data }); + + expect(merged.data).toEqual(data); + expect(merged.data).not.toBe(data); + }); + + it('clones config2 when it is an array', () => { + const data = [1, 2, 3]; + const merged = mergeConfig(config1, { data }); + + expect(merged.data).toEqual(data); + expect(merged.data).not.toBe(data); + }); + + it('sets config2 value directly for non-mergeable values', () => { + const obj = Object.create({}); + + expect(mergeConfig(config1, { data: 1 }).data).toBe(1); + expect(mergeConfig(config1, { data: 'str' }).data).toBe('str'); + expect(mergeConfig(config1, { data: obj }).data).toBe(obj); + expect(mergeConfig(config1, { data: null }).data).toBe(null); + }); + }); + + describe('mergeDeepPropertiesKeys', () => { + it('skips when both config1 and config2 values are undefined', () => { + expect(mergeConfig({ headers: undefined }, { headers: undefined })).toEqual({}); + }); + + it('merges when both values are plain objects', () => { + expect(mergeConfig({ headers: { a: 1, b: 1 } }, { headers: { b: 2, c: 2 } })).toEqual({ + headers: { a: 1, b: 2, c: 2 }, + }); + }); + + it('clones config2 when it is a plain object', () => { + const config1 = { headers: [1, 2, 3] }; + const config2 = { headers: { a: 1, b: 2 } }; + const merged = mergeConfig(config1, config2); + + expect(merged.headers).toEqual(config2.headers); + expect(merged.headers).not.toBe(config2.headers); + }); + + it('clones config2 when it is an array', () => { + const config1 = { headers: { a: 1, b: 1 } }; + const config2 = { headers: [1, 2, 3] }; + const merged = mergeConfig(config1, config2); + + expect(merged.headers).toEqual(config2.headers); + expect(merged.headers).not.toBe(config2.headers); + }); + + it('sets config2 value directly for non-mergeable values', () => { + const config1 = { headers: { a: 1, b: 1 } }; + const obj = Object.create({}); + + expect(mergeConfig(config1, { headers: 1 }).headers).toBe(1); + expect(mergeConfig(config1, { headers: 'str' }).headers).toBe('str'); + expect(mergeConfig(config1, { headers: obj }).headers).toBe(obj); + expect(mergeConfig(config1, { headers: null }).headers).toBe(null); + }); + + it('clones config1 when it is a plain object', () => { + const config1 = { headers: { a: 1, b: 2 } }; + const merged = mergeConfig(config1, {}); + + expect(merged.headers).toEqual(config1.headers); + expect(merged.headers).not.toBe(config1.headers); + }); + + it('clones config1 when it is an array', () => { + const config1 = { headers: [1, 2, 3] }; + const merged = mergeConfig(config1, {}); + + expect(merged.headers).toEqual(config1.headers); + expect(merged.headers).not.toBe(config1.headers); + }); + + it('sets config1 value directly for non-mergeable values', () => { + const obj = Object.create({}); + + expect(mergeConfig({ headers: 1 }, {}).headers).toBe(1); + expect(mergeConfig({ headers: 'str' }, {}).headers).toBe('str'); + expect(mergeConfig({ headers: obj }, {}).headers).toBe(obj); + expect(mergeConfig({ headers: null }, {}).headers).toBe(null); + }); + }); + + describe('defaultToConfig2Keys', () => { + it('skips when both config1 and config2 values are undefined', () => { + expect(mergeConfig({ transformRequest: undefined }, { transformRequest: undefined })).toEqual( + {} + ); + }); + + it('clones config2 when both values are plain objects', () => { + const config1 = { transformRequest: { a: 1, b: 1 } }; + const config2 = { transformRequest: { b: 2, c: 2 } }; + const merged = mergeConfig(config1, config2); + + expect(merged.transformRequest).toEqual(config2.transformRequest); + expect(merged.transformRequest).not.toBe(config2.transformRequest); + }); + + it('clones config2 when it is an array', () => { + const config1 = { transformRequest: { a: 1, b: 1 } }; + const config2 = { transformRequest: [1, 2, 3] }; + const merged = mergeConfig(config1, config2); + + expect(merged.transformRequest).toEqual(config2.transformRequest); + expect(merged.transformRequest).not.toBe(config2.transformRequest); + }); + + it('sets config2 value directly for non-mergeable values', () => { + const config1 = { transformRequest: { a: 1, b: 1 } }; + const obj = Object.create({}); + + expect(mergeConfig(config1, { transformRequest: 1 }).transformRequest).toBe(1); + expect(mergeConfig(config1, { transformRequest: 'str' }).transformRequest).toBe('str'); + expect(mergeConfig(config1, { transformRequest: obj }).transformRequest).toBe(obj); + expect(mergeConfig(config1, { transformRequest: null }).transformRequest).toBe(null); + }); + + it('clones config1 when it is a plain object', () => { + const config1 = { transformRequest: { a: 1, b: 2 } }; + const merged = mergeConfig(config1, {}); + + expect(merged.transformRequest).toEqual(config1.transformRequest); + expect(merged.transformRequest).not.toBe(config1.transformRequest); + }); + + it('clones config1 when it is an array', () => { + const config1 = { transformRequest: [1, 2, 3] }; + const merged = mergeConfig(config1, {}); + + expect(merged.transformRequest).toEqual(config1.transformRequest); + expect(merged.transformRequest).not.toBe(config1.transformRequest); + }); + + it('sets config1 value directly for non-mergeable values', () => { + const obj = Object.create({}); + + expect(mergeConfig({ transformRequest: 1 }, {}).transformRequest).toBe(1); + expect(mergeConfig({ transformRequest: 'str' }, {}).transformRequest).toBe('str'); + expect(mergeConfig({ transformRequest: obj }, {}).transformRequest).toBe(obj); + expect(mergeConfig({ transformRequest: null }, {}).transformRequest).toBe(null); + }); + }); + + describe('directMergeKeys', () => { + it('merges when config2 defines the key', () => { + expect(mergeConfig({}, { validateStatus: undefined })).toEqual({ validateStatus: undefined }); + }); + + it('merges when both values are plain objects', () => { + expect( + mergeConfig({ validateStatus: { a: 1, b: 1 } }, { validateStatus: { b: 2, c: 2 } }) + ).toEqual({ validateStatus: { a: 1, b: 2, c: 2 } }); + }); + + it('clones config2 when it is a plain object', () => { + const config1 = { validateStatus: [1, 2, 3] }; + const config2 = { validateStatus: { a: 1, b: 2 } }; + const merged = mergeConfig(config1, config2); + + expect(merged.validateStatus).toEqual(config2.validateStatus); + expect(merged.validateStatus).not.toBe(config2.validateStatus); + }); + + it('clones config2 when it is an array', () => { + const config1 = { validateStatus: { a: 1, b: 2 } }; + const config2 = { validateStatus: [1, 2, 3] }; + const merged = mergeConfig(config1, config2); + + expect(merged.validateStatus).toEqual(config2.validateStatus); + expect(merged.validateStatus).not.toBe(config2.validateStatus); + }); + + it('sets config2 value directly for non-mergeable values', () => { + const config1 = { validateStatus: { a: 1, b: 2 } }; + const obj = Object.create({}); + + expect(mergeConfig(config1, { validateStatus: 1 }).validateStatus).toBe(1); + expect(mergeConfig(config1, { validateStatus: 'str' }).validateStatus).toBe('str'); + expect(mergeConfig(config1, { validateStatus: obj }).validateStatus).toBe(obj); + expect(mergeConfig(config1, { validateStatus: null }).validateStatus).toBe(null); + }); + + it('clones config1 when it is a plain object', () => { + const config1 = { validateStatus: { a: 1, b: 2 } }; + const merged = mergeConfig(config1, {}); + + expect(merged.validateStatus).toEqual(config1.validateStatus); + expect(merged.validateStatus).not.toBe(config1.validateStatus); + }); + + it('clones config1 when it is an array', () => { + const config1 = { validateStatus: [1, 2, 3] }; + const merged = mergeConfig(config1, {}); + + expect(merged.validateStatus).toEqual(config1.validateStatus); + expect(merged.validateStatus).not.toBe(config1.validateStatus); + }); + + it('sets config1 value directly for non-mergeable values', () => { + const obj = Object.create({}); + + expect(mergeConfig({ validateStatus: 1 }, {}).validateStatus).toBe(1); + expect(mergeConfig({ validateStatus: 'str' }, {}).validateStatus).toBe('str'); + expect(mergeConfig({ validateStatus: obj }, {}).validateStatus).toBe(obj); + expect(mergeConfig({ validateStatus: null }, {}).validateStatus).toBe(null); + }); + }); +}); diff --git a/tests/unit/core/transformData.test.js b/tests/unit/core/transformData.test.js new file mode 100644 index 00000000..7eb07cb2 --- /dev/null +++ b/tests/unit/core/transformData.test.js @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; +import transformData from '../../../lib/core/transformData.js'; + +describe('core::transformData', () => { + it('supports a single transformer', () => { + const data = transformData.call({}, (value) => { + value = 'foo'; + return value; + }); + + expect(data).toBe('foo'); + }); + + it('supports an array of transformers', () => { + const data = transformData.call({ data: '' }, [ + (value) => value + 'f', + (value) => value + 'o', + (value) => value + 'o', + ]); + + expect(data).toBe('foo'); + }); + + it('passes headers through to transformers', () => { + const headers = { + 'content-type': 'foo/bar', + }; + + const data = transformData.call( + { + data: '', + headers, + }, + [(value, currentHeaders) => value + currentHeaders['content-type']] + ); + + expect(data).toBe('foo/bar'); + }); + + it('passes status code through to transformers', () => { + const data = transformData.call( + {}, + [(value, _headers, status) => value + status], + { data: '', status: 200 } + ); + + expect(data).toBe('200'); + }); +}); diff --git a/tests/unit/helpers/bind.test.js b/tests/unit/helpers/bind.test.js new file mode 100644 index 00000000..e8c7f5af --- /dev/null +++ b/tests/unit/helpers/bind.test.js @@ -0,0 +1,13 @@ +import { describe, it, expect } from 'vitest'; +import bind from '../../../lib/helpers/bind.js'; + +describe('bind', () => { + it('should bind an object to a function', () => { + const o = { val: 123 }; + const f = bind(function (num) { + return this.val * num; + }, o); + + expect(f(2)).toEqual(246); + }); +}); diff --git a/tests/unit/helpers/buildURL.test.js b/tests/unit/helpers/buildURL.test.js new file mode 100644 index 00000000..1ed78540 --- /dev/null +++ b/tests/unit/helpers/buildURL.test.js @@ -0,0 +1,126 @@ +import { describe, it, expect, vi } from 'vitest'; +import buildURL from '../../../lib/helpers/buildURL.js'; + +describe('helpers::buildURL', () => { + it('should support null params', () => { + expect(buildURL('/foo')).toEqual('/foo'); + }); + + it('should support params', () => { + expect( + buildURL('/foo', { + foo: 'bar', + isUndefined: undefined, + isNull: null, + }) + ).toEqual('/foo?foo=bar'); + }); + + it('should support sending raw params to custom serializer func', () => { + const serializer = vi.fn().mockReturnValue('foo=bar'); + const params = { foo: 'bar' }; + const options = { + serialize: serializer, + }; + expect( + buildURL( + '/foo', + { + foo: 'bar', + }, + options + ) + ).toEqual('/foo?foo=bar'); + expect(serializer).toHaveBeenCalledTimes(1); + expect(serializer).toHaveBeenCalledWith(params, options); + }); + + it('should support object params', () => { + expect( + buildURL('/foo', { + foo: { + bar: 'baz', + }, + }) + ).toEqual('/foo?foo%5Bbar%5D=baz'); + }); + + it('should support date params', () => { + const date = new Date(); + + expect( + buildURL('/foo', { + date, + }) + ).toEqual('/foo?date=' + date.toISOString()); + }); + + it('should support array params with encode', () => { + expect( + buildURL('/foo', { + foo: ['bar', 'baz'], + }) + ).toEqual('/foo?foo%5B%5D=bar&foo%5B%5D=baz'); + }); + + it('should support special char params', () => { + expect( + buildURL('/foo', { + foo: ':$, ', + }) + ).toEqual('/foo?foo=:$,+'); + }); + + it('should support existing params', () => { + expect( + buildURL('/foo?foo=bar', { + bar: 'baz', + }) + ).toEqual('/foo?foo=bar&bar=baz'); + }); + + it('should support "length" parameter', () => { + expect( + buildURL('/foo', { + query: 'bar', + start: 0, + length: 5, + }) + ).toEqual('/foo?query=bar&start=0&length=5'); + }); + + it('should correct discard url hash mark', () => { + expect( + buildURL('/foo?foo=bar#hash', { + query: 'baz', + }) + ).toEqual('/foo?foo=bar&query=baz'); + }); + + it('should support URLSearchParams', () => { + expect(buildURL('/foo', new URLSearchParams('bar=baz'))).toEqual('/foo?bar=baz'); + }); + + it('should support custom serialize function', () => { + const params = { + x: 1, + }; + + const options = { + serialize: (thisParams, thisOptions) => { + expect(thisParams).toEqual(params); + expect(thisOptions).toEqual(options); + return 'rendered'; + }, + }; + + expect(buildURL('/foo', params, options)).toEqual('/foo?rendered'); + + const customSerializer = (thisParams) => { + expect(thisParams).toEqual(params); + return 'rendered'; + }; + + expect(buildURL('/foo', params, customSerializer)).toEqual('/foo?rendered'); + }); +}); diff --git a/tests/unit/helpers/combineURLs.test.js b/tests/unit/helpers/combineURLs.test.js new file mode 100644 index 00000000..ac3ba610 --- /dev/null +++ b/tests/unit/helpers/combineURLs.test.js @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'vitest'; +import combineURLs from '../../../lib/helpers/combineURLs.js'; + +describe('helpers::combineURLs', () => { + it('should combine URLs', () => { + expect(combineURLs('https://api.github.com', '/users')).toBe('https://api.github.com/users'); + }); + + it('should remove duplicate slashes', () => { + expect(combineURLs('https://api.github.com/', '/users')).toBe('https://api.github.com/users'); + }); + + it('should insert missing slash', () => { + expect(combineURLs('https://api.github.com', 'users')).toBe('https://api.github.com/users'); + }); + + it('should not insert slash when relative url missing/empty', () => { + expect(combineURLs('https://api.github.com/users', '')).toBe('https://api.github.com/users'); + }); + + it('should allow a single slash for relative url', () => { + expect(combineURLs('https://api.github.com/users', '/')).toBe('https://api.github.com/users/'); + }); +}); diff --git a/tests/unit/helpers/formDataToJSON.test.js b/tests/unit/helpers/formDataToJSON.test.js new file mode 100644 index 00000000..cffb6383 --- /dev/null +++ b/tests/unit/helpers/formDataToJSON.test.js @@ -0,0 +1,72 @@ +import { describe, it, expect } from 'vitest'; +import formDataToJSON from '../../../lib/helpers/formDataToJSON.js'; + +describe('formDataToJSON', () => { + it('should convert a FormData Object to JSON Object', () => { + const formData = new FormData(); + + formData.append('foo[bar][baz]', '123'); + + expect(formDataToJSON(formData)).toEqual({ + foo: { + bar: { + baz: '123', + }, + }, + }); + }); + + it('should convert repeatable values as an array', () => { + const formData = new FormData(); + + formData.append('foo', '1'); + formData.append('foo', '2'); + + expect(formDataToJSON(formData)).toEqual({ + foo: ['1', '2'], + }); + }); + + it('should convert props with empty brackets to arrays', () => { + const formData = new FormData(); + + formData.append('foo[]', '1'); + formData.append('foo[]', '2'); + + expect(formDataToJSON(formData)).toEqual({ + foo: ['1', '2'], + }); + }); + + it('should supported indexed arrays', () => { + const formData = new FormData(); + + formData.append('foo[0]', '1'); + formData.append('foo[1]', '2'); + + expect(formDataToJSON(formData)).toEqual({ + foo: ['1', '2'], + }); + }); + + it('should resist prototype pollution CVE', () => { + const formData = new FormData(); + + formData.append('foo[0]', '1'); + formData.append('foo[1]', '2'); + formData.append('__proto__.x', 'hack'); + formData.append('constructor.prototype.y', 'value'); + + expect(formDataToJSON(formData)).toEqual({ + foo: ['1', '2'], + constructor: { + prototype: { + y: 'value', + }, + }, + }); + + expect({}.x).toEqual(undefined); + expect({}.y).toEqual(undefined); + }); +}); diff --git a/tests/unit/helpers/isAbsoluteURL.test.js b/tests/unit/helpers/isAbsoluteURL.test.js new file mode 100644 index 00000000..ba4f95d8 --- /dev/null +++ b/tests/unit/helpers/isAbsoluteURL.test.js @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'vitest'; +import isAbsoluteURL from '../../../lib/helpers/isAbsoluteURL.js'; + +describe('helpers::isAbsoluteURL', () => { + it('should return true if URL begins with valid scheme name', () => { + expect(isAbsoluteURL('https://api.github.com/users')).toBe(true); + expect(isAbsoluteURL('custom-scheme-v1.0://example.com/')).toBe(true); + expect(isAbsoluteURL('HTTP://example.com/')).toBe(true); + }); + + it('should return false if URL begins with invalid scheme name', () => { + expect(isAbsoluteURL('123://example.com/')).toBe(false); + expect(isAbsoluteURL('!valid://example.com/')).toBe(false); + }); + + it('should return true if URL is protocol-relative', () => { + expect(isAbsoluteURL('//example.com/')).toBe(true); + }); + + it('should return false if URL is relative', () => { + expect(isAbsoluteURL('/foo')).toBe(false); + expect(isAbsoluteURL('foo')).toBe(false); + }); +}); diff --git a/tests/unit/helpers/isAxiosError.test.js b/tests/unit/helpers/isAxiosError.test.js new file mode 100644 index 00000000..2c6e3d08 --- /dev/null +++ b/tests/unit/helpers/isAxiosError.test.js @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest'; +import AxiosError from '../../../lib/core/AxiosError.js'; +import isAxiosError from '../../../lib/helpers/isAxiosError.js'; + +describe('helpers::isAxiosError', () => { + it('should return true if the error is created by core::createError', () => { + expect(isAxiosError(new AxiosError('Boom!', null, { foo: 'bar' }))).toBe(true); + }); + + it('should return true if the error is enhanced by core::enhanceError', () => { + expect(isAxiosError(AxiosError.from(new Error('Boom!'), null, { foo: 'bar' }))).toBe(true); + }); + + it('should return false if the error is a normal Error instance', () => { + expect(isAxiosError(new Error('Boom!'))).toBe(false); + }); + + it('should return false if the error is null', () => { + expect(isAxiosError(null)).toBe(false); + }); +}); diff --git a/tests/unit/helpers/parseHeaders.test.js b/tests/unit/helpers/parseHeaders.test.js new file mode 100644 index 00000000..a6dc30b4 --- /dev/null +++ b/tests/unit/helpers/parseHeaders.test.js @@ -0,0 +1,43 @@ +import { describe, it, expect } from 'vitest'; +import parseHeaders from '../../../lib/helpers/parseHeaders.js'; + +describe('helpers::parseHeaders', () => { + it('should parse headers', () => { + const date = new Date(); + const parsed = parseHeaders( + 'Date: ' + + date.toISOString() + + '\n' + + 'Content-Type: application/json\n' + + 'Connection: keep-alive\n' + + 'Transfer-Encoding: chunked' + ); + + expect(parsed.date).toEqual(date.toISOString()); + expect(parsed['content-type']).toEqual('application/json'); + expect(parsed.connection).toEqual('keep-alive'); + expect(parsed['transfer-encoding']).toEqual('chunked'); + }); + + it('should use array for set-cookie', () => { + const parsedZero = parseHeaders(''); + const parsedSingle = parseHeaders('Set-Cookie: key=val;'); + const parsedMulti = parseHeaders('Set-Cookie: key=val;\n' + 'Set-Cookie: key2=val2;\n'); + + expect(parsedZero['set-cookie']).toBeUndefined(); + expect(parsedSingle['set-cookie']).toEqual(['key=val;']); + expect(parsedMulti['set-cookie']).toEqual(['key=val;', 'key2=val2;']); + }); + + it('should handle duplicates', () => { + const parsed = parseHeaders( + 'Age: age-a\n' + + 'Age: age-b\n' + + 'Foo: foo-a\n' + + 'Foo: foo-b\n' + ); + + expect(parsed.age).toEqual('age-a'); + expect(parsed.foo).toEqual('foo-a, foo-b'); + }); +}); diff --git a/tests/unit/helpers/spread.test.js b/tests/unit/helpers/spread.test.js new file mode 100644 index 00000000..02e92b78 --- /dev/null +++ b/tests/unit/helpers/spread.test.js @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest'; +import spread from '../../../lib/helpers/spread.js'; + +describe('helpers::spread', () => { + it('should spread array to arguments', () => { + let value = 0; + spread((a, b) => { + value = a * b; + })([5, 10]); + + expect(value).toEqual(50); + }); + + it('should return callback result', () => { + const value = spread((a, b) => { + return a * b; + })([5, 10]); + + expect(value).toEqual(50); + }); +}); diff --git a/tests/unit/helpers/validator.test.js b/tests/unit/helpers/validator.test.js new file mode 100644 index 00000000..27dcd259 --- /dev/null +++ b/tests/unit/helpers/validator.test.js @@ -0,0 +1,67 @@ +import { describe, it, expect } from 'vitest'; +import AxiosError from '../../../lib/core/AxiosError.js'; +import validator from '../../../lib/helpers/validator.js'; + +describe('validator::assertOptions', () => { + it('should throw only if unknown an option was passed', () => { + let error; + try { + validator.assertOptions( + { + x: true, + }, + { + y: validator.validators.boolean, + } + ); + } catch (err) { + error = err; + } + expect(error).toBeInstanceOf(AxiosError); + expect(error.message).toBe('Unknown option x'); + expect(error.code).toBe(AxiosError.ERR_BAD_OPTION); + + expect(() => { + validator.assertOptions( + { + x: true, + }, + { + x: validator.validators.boolean, + y: validator.validators.boolean, + } + ); + }).not.toThrow(new Error('Unknown option x')); + }); + + it("should throw TypeError only if option type doesn't match", () => { + let error; + try { + validator.assertOptions( + { + x: 123, + }, + { + x: validator.validators.boolean, + } + ); + } catch (err) { + error = err; + } + expect(error).toBeInstanceOf(AxiosError); + expect(error.message).toBe('option x must be a boolean'); + expect(error.code).toBe(AxiosError.ERR_BAD_OPTION_VALUE); + + expect(() => { + validator.assertOptions( + { + x: true, + }, + { + x: validator.validators.boolean, + y: validator.validators.boolean, + } + ); + }).not.toThrow(); + }); +}); diff --git a/tests/unit/utils/endsWith.test.js b/tests/unit/utils/endsWith.test.js new file mode 100644 index 00000000..bca50567 --- /dev/null +++ b/tests/unit/utils/endsWith.test.js @@ -0,0 +1,13 @@ +import { describe, it, expect } from 'vitest'; +import utils from '../../../lib/utils.js'; + +const { kindOf } = utils; + +describe('utils::kindOf', () => { + it('should return object tag', () => { + expect(kindOf({})).toEqual('object'); + // cached result + expect(kindOf({})).toEqual('object'); + expect(kindOf([])).toEqual('array'); + }); +}); diff --git a/tests/unit/utils/extend.test.js b/tests/unit/utils/extend.test.js new file mode 100644 index 00000000..d436f48f --- /dev/null +++ b/tests/unit/utils/extend.test.js @@ -0,0 +1,40 @@ +import { describe, it, expect } from 'vitest'; +import utils from '../../../lib/utils.js'; + +const { extend } = utils; + +describe('utils::extend', () => { + it('should be mutable', () => { + const a = {}; + const b = { foo: 123 }; + + extend(a, b); + + expect(a.foo).toEqual(b.foo); + }); + + it('should extend properties', () => { + let a = { foo: 123, bar: 456 }; + const b = { bar: 789 }; + + a = extend(a, b); + + expect(a.foo).toEqual(123); + expect(a.bar).toEqual(789); + }); + + it('should bind to thisArg', () => { + const a = {}; + const b = { + getFoo: function getFoo() { + return this.foo; + }, + }; + const thisArg = { foo: 'barbaz' }; + + extend(a, b, thisArg); + + expect(typeof a.getFoo).toEqual('function'); + expect(a.getFoo()).toEqual(thisArg.foo); + }); +}); diff --git a/tests/unit/utils/forEach.test.js b/tests/unit/utils/forEach.test.js new file mode 100644 index 00000000..756aaf91 --- /dev/null +++ b/tests/unit/utils/forEach.test.js @@ -0,0 +1,69 @@ +import { describe, it, expect } from 'vitest'; +import utils from '../../../lib/utils.js'; + +const { forEach } = utils; + +describe('utils::forEach', () => { + it('should loop over an array', () => { + let sum = 0; + + forEach([1, 2, 3, 4, 5], (val) => { + sum += val; + }); + + expect(sum).toEqual(15); + }); + + it('should loop over object keys', () => { + let keys = ''; + let vals = 0; + const obj = { + b: 1, + a: 2, + r: 3, + }; + + forEach(obj, (v, k) => { + keys += k; + vals += v; + }); + + expect(keys).toEqual('bar'); + expect(vals).toEqual(6); + }); + + it('should handle undefined gracefully', () => { + let count = 0; + + forEach(undefined, () => { + count++; + }); + + expect(count).toEqual(0); + }); + + it('should make an array out of non-array argument', () => { + let count = 0; + + forEach( + () => {}, + () => { + count++; + } + ); + + expect(count).toEqual(1); + }); + + it('should handle non object prototype gracefully', () => { + let count = 0; + const data = Object.create(null); + data.foo = 'bar'; + + forEach(data, () => { + count++; + }); + + expect(count).toEqual(1); + }); +}); diff --git a/tests/unit/utils/isX.test.js b/tests/unit/utils/isX.test.js new file mode 100644 index 00000000..21f4bab1 --- /dev/null +++ b/tests/unit/utils/isX.test.js @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'vitest'; +import utils from '../../../lib/utils.js'; + +describe('utils::isX', () => { + it('should validate Array', () => { + expect(utils.isArray([])).toEqual(true); + expect(utils.isArray({ length: 5 })).toEqual(false); + }); + + it('should validate ArrayBuffer', () => { + expect(utils.isArrayBuffer(new ArrayBuffer(2))).toEqual(true); + expect(utils.isArrayBuffer({})).toEqual(false); + }); + + it('should validate ArrayBufferView', () => { + expect(utils.isArrayBufferView(new DataView(new ArrayBuffer(2)))).toEqual(true); + }); + + it('should validate FormData', () => { + expect(utils.isFormData(new FormData())).toEqual(true); + }); + + it('should validate Blob', () => { + expect(utils.isBlob(new Blob())).toEqual(true); + }); + + it('should validate String', () => { + expect(utils.isString('')).toEqual(true); + expect( + utils.isString({ + toString: function () { + return ''; + }, + }) + ).toEqual(false); + }); + + it('should validate Number', () => { + expect(utils.isNumber(123)).toEqual(true); + expect(utils.isNumber('123')).toEqual(false); + }); + + it('should validate Undefined', () => { + expect(utils.isUndefined()).toEqual(true); + expect(utils.isUndefined(null)).toEqual(false); + }); + + it('should validate Object', () => { + expect(utils.isObject({})).toEqual(true); + expect(utils.isObject([])).toEqual(true); + expect(utils.isObject(null)).toEqual(false); + }); + + it('should validate plain Object', () => { + expect(utils.isPlainObject({})).toEqual(true); + expect(utils.isPlainObject([])).toEqual(false); + expect(utils.isPlainObject(null)).toEqual(false); + expect(utils.isPlainObject(Object.create({}))).toEqual(false); + }); + + it('should validate Date', () => { + expect(utils.isDate(new Date())).toEqual(true); + expect(utils.isDate(Date.now())).toEqual(false); + }); + + it('should validate Function', () => { + expect(utils.isFunction(function () {})).toEqual(true); + expect(utils.isFunction('function')).toEqual(false); + }); + + it('should validate URLSearchParams', () => { + expect(utils.isURLSearchParams(new URLSearchParams())).toEqual(true); + expect(utils.isURLSearchParams('foo=1&bar=2')).toEqual(false); + }); + + it('should validate TypedArray instance', () => { + expect(utils.isTypedArray(new Uint8Array([1, 2, 3]))).toEqual(true); + expect(utils.isTypedArray([1, 2, 3])).toEqual(false); + }); +}); diff --git a/tests/unit/utils/kindOf.test.js b/tests/unit/utils/kindOf.test.js new file mode 100644 index 00000000..bca50567 --- /dev/null +++ b/tests/unit/utils/kindOf.test.js @@ -0,0 +1,13 @@ +import { describe, it, expect } from 'vitest'; +import utils from '../../../lib/utils.js'; + +const { kindOf } = utils; + +describe('utils::kindOf', () => { + it('should return object tag', () => { + expect(kindOf({})).toEqual('object'); + // cached result + expect(kindOf({})).toEqual('object'); + expect(kindOf([])).toEqual('array'); + }); +}); diff --git a/tests/unit/utils/kindOfTest.test.js b/tests/unit/utils/kindOfTest.test.js new file mode 100644 index 00000000..8dfb2be1 --- /dev/null +++ b/tests/unit/utils/kindOfTest.test.js @@ -0,0 +1,12 @@ +import { describe, it, expect } from 'vitest'; +import utils from '../../../lib/utils.js'; + +describe('utils::kindOfTest', () => { + it('should return true if the type is matched', () => { + const { kindOfTest } = utils; + const test = kindOfTest('number'); + + expect(test(123)).toEqual(true); + expect(test('123')).toEqual(false); + }); +}); diff --git a/tests/unit/utils/merge.test.js b/tests/unit/utils/merge.test.js new file mode 100644 index 00000000..76f2106a --- /dev/null +++ b/tests/unit/utils/merge.test.js @@ -0,0 +1,97 @@ +import { describe, it, expect } from 'vitest'; +import utils from '../../../lib/utils.js'; + +const { merge } = utils; + +describe('utils::merge', () => { + it('should be immutable', () => { + const a = {}; + const b = { foo: 123 }; + const c = { bar: 456 }; + + merge(a, b, c); + + expect(typeof a.foo).toEqual('undefined'); + expect(typeof a.bar).toEqual('undefined'); + expect(typeof b.bar).toEqual('undefined'); + expect(typeof c.foo).toEqual('undefined'); + }); + + it('should merge properties', () => { + const a = { foo: 123 }; + const b = { bar: 456 }; + const c = { foo: 789 }; + const d = merge(a, b, c); + + expect(d.foo).toEqual(789); + expect(d.bar).toEqual(456); + }); + + it('should merge recursively', () => { + const a = { foo: { bar: 123 } }; + const b = { foo: { baz: 456 }, bar: { qux: 789 } }; + + expect(merge(a, b)).toEqual({ + foo: { + bar: 123, + baz: 456, + }, + bar: { + qux: 789, + }, + }); + }); + + it('should remove all references from nested objects', () => { + const a = { foo: { bar: 123 } }; + const b = {}; + const d = merge(a, b); + + expect(d).toEqual({ + foo: { + bar: 123, + }, + }); + + expect(d.foo).not.toBe(a.foo); + }); + + it('handles null and undefined arguments', () => { + expect(merge(undefined, undefined)).toEqual({}); + expect(merge(undefined, { foo: 123 })).toEqual({ foo: 123 }); + expect(merge({ foo: 123 }, undefined)).toEqual({ foo: 123 }); + + expect(merge(null, null)).toEqual({}); + expect(merge(null, { foo: 123 })).toEqual({ foo: 123 }); + expect(merge({ foo: 123 }, null)).toEqual({ foo: 123 }); + }); + + it('should replace properties with null', () => { + expect(merge({}, { a: null })).toEqual({ a: null }); + expect(merge({ a: null }, {})).toEqual({ a: null }); + }); + + it('should replace properties with arrays', () => { + expect(merge({}, { a: [1, 2, 3] })).toEqual({ a: [1, 2, 3] }); + expect(merge({ a: 2 }, { a: [1, 2, 3] })).toEqual({ a: [1, 2, 3] }); + expect(merge({ a: { b: 2 } }, { a: [1, 2, 3] })).toEqual({ a: [1, 2, 3] }); + }); + + it('should replace properties with cloned arrays', () => { + const a = [1, 2, 3]; + const d = merge({}, { a }); + + expect(d).toEqual({ a: [1, 2, 3] }); + expect(d.a).not.toBe(a); + }); + + it('should support caseless option', () => { + const a = { x: 1 }; + const b = { X: 2 }; + const merged = merge.call({ caseless: true }, a, b); + + expect(merged).toEqual({ + x: 2, + }); + }); +}); diff --git a/tests/unit/utils/toArray.test.js b/tests/unit/utils/toArray.test.js new file mode 100644 index 00000000..184391dd --- /dev/null +++ b/tests/unit/utils/toArray.test.js @@ -0,0 +1,13 @@ +import { describe, it, expect } from 'vitest'; +import utils from '../../../lib/utils.js'; + +const { toArray } = utils; + +describe('utils::toArray', () => { + it('should return null or an array copy depending on input', () => { + expect(toArray()).toEqual(null); + expect(toArray([])).toEqual([]); + expect(toArray([1])).toEqual([1]); + expect(toArray([1, 2, 3])).toEqual([1, 2, 3]); + }); +}); diff --git a/tests/unit/utils/toFlatObject.test.js b/tests/unit/utils/toFlatObject.test.js new file mode 100644 index 00000000..ecc9f32e --- /dev/null +++ b/tests/unit/utils/toFlatObject.test.js @@ -0,0 +1,13 @@ +import { describe, it, expect } from 'vitest'; +import utils from '../../../lib/utils.js'; + +const { toFlatObject } = utils; + +describe('utils::toFlatObject', () => { + it('should resolve object proto chain to a flat object representation', () => { + const a = { x: 1 }; + const b = Object.create(a, { y: { value: 2 } }); + const c = Object.create(b, { z: { value: 3 } }); + expect(toFlatObject(c)).toEqual({ x: 1, y: 2, z: 3 }); + }); +}); diff --git a/tests/unit/utils/trim.test.js b/tests/unit/utils/trim.test.js new file mode 100644 index 00000000..07c5c243 --- /dev/null +++ b/tests/unit/utils/trim.test.js @@ -0,0 +1,12 @@ +import { describe, it, expect } from 'vitest'; +import utils from '../../../lib/utils.js'; + +describe('utils::trim', () => { + it('should trim spaces', () => { + expect(utils.trim(' foo ')).toEqual('foo'); + }); + + it('should trim tabs', () => { + expect(utils.trim('\tfoo\t')).toEqual('foo'); + }); +}); diff --git a/vitest.config.js b/vitest.config.js index 0a4b7144..059989c4 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -25,6 +25,22 @@ export default defineConfig({ setupFiles: ['tests/setup/browser.setup.js'], }, }, + { + test: { + name: 'browser-headless', + include: ['tests/browser/**/*.browser.test.js'], + browser: { + enabled: true, + provider: playwright(), + instances: [ + { browser: 'chromium', headless: true }, + { browser: 'firefox', headless: true }, + { browser: 'webkit', headless: true }, + ], + }, + setupFiles: ['tests/setup/browser.setup.js'], + }, + }, ], }, });