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
This commit is contained in:
Jay 2026-03-12 15:27:09 +02:00 committed by GitHub
parent f7a4ee21d5
commit d905b7598d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
90 changed files with 12796 additions and 431 deletions

View File

@ -15,12 +15,43 @@ concurrency:
jobs: jobs:
ci: ci:
runs-on: ubuntu-latest 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: strategy:
matrix:
node-version: [14.x, 16.x, 18.x, 20.x, 22.x, 24.x]
fail-fast: false fail-fast: false
matrix:
node-version: [12, 14, 16, 18]
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v6 uses: actions/checkout@v6
@ -31,21 +62,51 @@ jobs:
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: npm cache: npm
- name: Install dependencies Node 14 cache-dependency-path: tests/smoke/cjs/package-lock.json
if: matrix.node-version == '14.x'
run: npm i
- name: Install dependencies - name: Install dependencies
if: matrix.node-version != '14.x' if: matrix.node-version == 16 || matrix.node-version == 18
run: npm ci run: npm ci
- name: Build project - name: Download build artifact
run: npm run build uses: actions/download-artifact@v8
- name: Run unit tests with:
run: npm run test:node name: axios
- name: Run package tests path: dist
run: npm run test:package - name: Install CJS smoke test dependencies
- name: Run browser tests working-directory: tests/smoke/cjs
run: npm run test:browser run: npm install
if: matrix.node-version == '24.x' - name: Run CJS smoke tests
- name: Dependency Review working-directory: tests/smoke/cjs
uses: actions/dependency-review-action@v4 run: npm run test:smoke:cjs:mocha
if: matrix.node-version == '24.x'
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

3
.gitignore vendored
View File

@ -13,4 +13,5 @@ backup/
.npmrc .npmrc
.env .env
dist/ dist/
.vscode/ .vscode/
docs/

View File

@ -52,6 +52,7 @@
"test:vitest": "vitest run", "test:vitest": "vitest run",
"test:vitest:unit": "vitest run --project unit", "test:vitest:unit": "vitest run --project unit",
"test:vitest:browser": "vitest run --project browser", "test:vitest:browser": "vitest run --project browser",
"test:vitest:browser:headless": "vitest run --project browser-headless",
"test:vitest:watch": "vitest", "test:vitest:watch": "vitest",
"test:package": "npm run test:eslint && npm run test:exports", "test:package": "npm run test:eslint && npm run test:exports",
"test:eslint": "node bin/ssl_hotfix.js eslint lib/**/*.js", "test:eslint": "node bin/ssl_hotfix.js eslint lib/**/*.js",

View File

@ -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;
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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.');
});
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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;
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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();
});
});

View File

@ -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');
});
});

View File

@ -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&param2=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:',
});
});
});

View File

@ -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);
});
});

View File

@ -1,10 +0,0 @@
import { expect, test } from 'vitest';
test('runs in browser environment', () => {
document.body.innerHTML = '<div data-testid="smoke">vitest browser smoke</div>';
const el = document.querySelector('[data-testid="smoke"]');
expect(el?.textContent).toBe('vitest browser smoke');
expect(globalThis.window).toBeDefined();
});

View File

@ -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]);
});
});

View File

@ -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' });
});
});

View File

@ -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);
});
});
});

View File

@ -6,8 +6,6 @@ import { Throttle } from 'stream-throttle';
import formidable from 'formidable'; import formidable from 'formidable';
import selfsigned from 'selfsigned'; 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 SERVER_HANDLER_STREAM_ECHO = (req, res) => req.pipe(res);
export const setTimeoutAsync = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); export const setTimeoutAsync = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

1305
tests/smoke/cjs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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;
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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,
});
});
});

View File

@ -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');
});
});

View File

@ -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 });
});
});

View File

@ -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');
});
});

1549
tests/smoke/esm/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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();
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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,
});
});
});

View File

@ -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');
});
});

View File

@ -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 });
});
});

View File

@ -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');
});
});

View File

@ -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: [],
},
},
],
},
});

View File

@ -1,9 +1,8 @@
import { afterEach, describe, it, vi } from 'vitest'; import { describe, it, vi } from 'vitest';
import assert from 'assert'; import assert from 'assert';
import { import {
startHTTPServer, startHTTPServer,
stopHTTPServer, stopHTTPServer,
LOCAL_SERVER_URL,
setTimeoutAsync, setTimeoutAsync,
makeReadableStream, makeReadableStream,
generateReadable, generateReadable,
@ -15,6 +14,9 @@ import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill.js';
import util from 'util'; import util from 'util';
import NodeFormData from 'form-data'; import NodeFormData from 'form-data';
const SERVER_PORT = 8010;
const LOCAL_SERVER_URL = `http://localhost:${SERVER_PORT}`;
const pipelineAsync = util.promisify(stream.pipeline); const pipelineAsync = util.promisify(stream.pipeline);
const fetchAxios = axios.create({ const fetchAxios = axios.create({
@ -22,69 +24,85 @@ const fetchAxios = axios.create({
adapter: 'fetch', adapter: 'fetch',
}); });
let server;
describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => { describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => {
afterEach(async () => {
await stopHTTPServer(server);
server = null;
});
describe('responses', () => { describe('responses', () => {
it('should support text response type', async () => { it('should support text response type', async () => {
const originalData = 'my data'; const originalData = 'my data';
server = await startHTTPServer((req, res) => res.end(originalData)); const server = await startHTTPServer((req, res) => res.end(originalData), {
port: SERVER_PORT,
const { data } = await fetchAxios.get('/', {
responseType: 'text',
}); });
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 () => { it('should support arraybuffer response type', async () => {
const originalData = 'my data'; const originalData = 'my data';
server = await startHTTPServer((req, res) => res.end(originalData)); const server = await startHTTPServer((req, res) => res.end(originalData), {
port: SERVER_PORT,
const { data } = await fetchAxios.get('/', {
responseType: 'arraybuffer',
}); });
assert.deepStrictEqual( try {
data, const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, {
Uint8Array.from(await new TextEncoder().encode(originalData)).buffer responseType: 'arraybuffer',
); });
assert.deepStrictEqual(
data,
Uint8Array.from(await new TextEncoder().encode(originalData)).buffer
);
} finally {
await stopHTTPServer(server);
}
}); });
it('should support blob response type', async () => { it('should support blob response type', async () => {
const originalData = 'my data'; const originalData = 'my data';
server = await startHTTPServer((req, res) => res.end(originalData)); const server = await startHTTPServer((req, res) => res.end(originalData), {
port: SERVER_PORT,
const { data } = await fetchAxios.get('/', {
responseType: 'blob',
}); });
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 () => { it('should support stream response type', async () => {
const originalData = 'my data'; const originalData = 'my data';
server = await startHTTPServer((req, res) => res.end(originalData)); const server = await startHTTPServer((req, res) => res.end(originalData), {
port: SERVER_PORT,
const { data } = await fetchAxios.get('/', {
responseType: 'stream',
}); });
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 () => { it('should support formData response type', async () => {
@ -92,295 +110,382 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () =>
originalData.append('x', '123'); originalData.append('x', '123');
server = await startHTTPServer(async (req, res) => { const server = await startHTTPServer(
const response = await new Response(originalData); 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()); res.end(await response.text());
}); },
{ port: SERVER_PORT }
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())
); );
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); }, 5000);
it('should support json response type', async () => { it('should support json response type', async () => {
const originalData = { x: 'my data' }; const originalData = { x: 'my data' };
server = await startHTTPServer((req, res) => res.end(JSON.stringify(originalData))); const server = await startHTTPServer((req, res) => res.end(JSON.stringify(originalData)), {
port: SERVER_PORT,
const { data } = await fetchAxios.get('/', {
responseType: 'json',
}); });
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('progress', () => {
describe('upload', () => { describe('upload', () => {
it('should support upload progress capturing', async () => { it('should support upload progress capturing', async () => {
server = await startHTTPServer({ const server = await startHTTPServer(
rate: 100 * 1024, {
}); rate: 100 * 1024,
},
let content = ''; { port: SERVER_PORT }
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 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, { const readable = stream.Readable.from(
onUploadProgress: ({ loaded, total, progress, bytes, upload }) => { (async function* () {
console.log( let i = count;
`Upload Progress ${loaded} from ${total} bytes (${(progress * 100).toFixed(1)}%)`
);
samples.push({ while (i-- > 0) {
loaded, await setTimeoutAsync(1100);
total, content += chunk;
progress, yield chunk;
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,
};
} }
})() })()
) );
);
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); }, 15000);
it('should not fail with get method', async () => { 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('/', { try {
onUploadProgress() {}, 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', () => { describe('download', () => {
it('should support download progress capturing', async () => { it('should support download progress capturing', async () => {
server = await startHTTPServer({ const server = await startHTTPServer(
rate: 100 * 1024, {
}); rate: 100 * 1024,
},
let content = ''; {
const count = 10; port: SERVER_PORT,
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 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, { const readable = stream.Readable.from(
onDownloadProgress: ({ loaded, total, progress, bytes, download }) => { (async function* () {
console.log( let i = count;
`Download Progress ${loaded} from ${total} bytes (${(progress * 100).toFixed(1)}%)`
);
samples.push({ while (i-- > 0) {
loaded, await setTimeoutAsync(1100);
total, content += chunk;
progress, yield chunk;
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,
};
} }
})() })()
) );
);
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); }, 15000);
}); });
}); });
it('should support basic auth', async () => { 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'; try {
const headers = { Authorization: 'Bearer 1234' }; const user = 'foo';
const res = await axios.get(`http://${user}@localhost:4444/`, { headers }); 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'); const base64 = Buffer.from(`${user}:`, 'utf8').toString('base64');
assert.equal(res.data, `Basic ${base64}`); assert.equal(res.data, `Basic ${base64}`);
} finally {
await stopHTTPServer(server);
}
}); });
it('should support stream.Readable as a payload', async () => { 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', () => { describe('request aborting', () => {
it('should be able to abort the request stream', async () => { it('should be able to abort the request stream', async () => {
server = await startHTTPServer({ const server = await startHTTPServer(
rate: 100000, {
useBuffering: true, rate: 100000,
}); useBuffering: true,
},
{ port: SERVER_PORT }
);
const controller = new AbortController(); try {
const controller = new AbortController();
setTimeout(() => { setTimeout(() => {
controller.abort(); controller.abort();
}, 500); }, 500);
await assert.rejects(async () => { await assert.rejects(async () => {
await fetchAxios.post('/', makeReadableStream(), { await fetchAxios.post(
responseType: 'stream', `http://localhost:${server.address().port}/`,
signal: controller.signal, makeReadableStream(),
}); {
}, /CanceledError/); responseType: 'stream',
signal: controller.signal,
}
);
}, /CanceledError/);
} finally {
await stopHTTPServer(server);
}
}); });
it('should be able to abort the response stream', async () => { it('should be able to abort the response stream', async () => {
server = await startHTTPServer((req, res) => { const server = await startHTTPServer(
pipelineAsync(generateReadable(10000, 10), res).catch(() => { (req, res) => {
// Client-side abort intentionally closes the stream early in this test. 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(); await assert.rejects(async () => {
await data.pipeTo(makeEchoStream());
setTimeout(() => { }, /^(AbortError|CanceledError):/);
controller.abort(new Error('test')); } finally {
}, 800); await stopHTTPServer(server);
}
const { data } = await fetchAxios.get('/', {
responseType: 'stream',
signal: controller.signal,
});
await assert.rejects(async () => {
await data.pipeTo(makeEchoStream());
}, /^(AbortError|CanceledError):/);
}); });
}); });
it('should support a timeout', async () => { it('should support a timeout', async () => {
server = await startHTTPServer(async (req, res) => { const server = await startHTTPServer(
await setTimeoutAsync(1000); async (req, res) => {
res.end('OK'); 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 assert.rejects(async () => {
await fetchAxios('/', { await fetchAxios(`http://localhost:${server.address().port}/`, {
timeout, timeout,
}); });
}, /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 () => { 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); } finally {
assert.equal(res.config.url, '/foo'); await stopHTTPServer(server);
}
}); });
it('should support params', async () => { 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', { assert.strictEqual(data, '/?test=1&foo=1&bar=2');
params: { } finally {
foo: 1, await stopHTTPServer(server);
bar: 2, }
},
});
assert.strictEqual(data, '/?test=1&foo=1&bar=2');
}); });
it('should handle fetch failed error as an AxiosError with ERR_NETWORK code', async () => { 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 () => { it('should get response headers', async () => {
server = await startHTTPServer((req, res) => { const server = await startHTTPServer(
res.setHeader('foo', 'bar'); (req, res) => {
res.end(req.url); res.setHeader('foo', 'bar');
}); res.end(req.url);
},
{ port: SERVER_PORT }
);
const { headers } = await fetchAxios.get('/', { try {
responseType: 'stream', 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', () => { describe('fetch adapter - Content-Type handling', () => {
@ -411,13 +523,20 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () =>
const form = new NodeFormData(); const form = new NodeFormData();
form.append('foo', 'bar'); form.append('foo', 'bar');
server = await startHTTPServer((req, res) => { const server = await startHTTPServer(
const contentType = req.headers['content-type']; (req, res) => {
assert.match(contentType, /^multipart\/form-data; boundary=/i); const contentType = req.headers['content-type'];
res.end('OK'); 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 () => { 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('/', { try {
env: { const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, {
fetch: undefined, 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 () => { 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 { const server = await startHTTPServer((req, res) => res.end('OK'), { port: SERVER_PORT });
server = await startHTTPServer((req, res) => res.end('OK'));
const { data } = await fetchAxios.get('/', { try {
const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, {
env: { env: {
fetch: undefined, fetch: undefined,
}, },
@ -525,6 +648,7 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () =>
assert.strictEqual(data, 'global'); assert.strictEqual(data, 'global');
} finally { } finally {
vi.stubGlobal('fetch', globalFetch); vi.stubGlobal('fetch', globalFetch);
await stopHTTPServer(server);
} }
}); });
}); });

View File

@ -31,6 +31,11 @@ import bodyParser from 'body-parser';
import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill.js'; import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill.js';
import { lookup } from 'dns'; 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', () => { describe('supports http with nodejs', () => {
const adaptersTestsDir = path.join(process.cwd(), 'tests/unit/adapters'); const adaptersTestsDir = path.join(process.cwd(), 'tests/unit/adapters');
const thisTestFilePath = path.join(adaptersTestsDir, 'http.test.js'); const thisTestFilePath = path.join(adaptersTestsDir, 'http.test.js');
@ -59,7 +64,7 @@ describe('supports http with nodejs', () => {
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(data)); res.end(JSON.stringify(data));
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -82,7 +87,7 @@ describe('supports http with nodejs', () => {
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(data)); res.end(JSON.stringify(data));
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -102,7 +107,7 @@ describe('supports http with nodejs', () => {
res.end(); res.end();
}, 1000); }, 1000);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -128,7 +133,7 @@ describe('supports http with nodejs', () => {
res.end(); res.end();
}, 1000); }, 1000);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -154,7 +159,7 @@ describe('supports http with nodejs', () => {
res.end(); res.end();
}, 1000); }, 1000);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -180,7 +185,7 @@ describe('supports http with nodejs', () => {
res.end(); res.end();
}, 1000); }, 1000);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -212,7 +217,7 @@ describe('supports http with nodejs', () => {
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(data)); res.end(JSON.stringify(data));
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -237,7 +242,7 @@ describe('supports http with nodejs', () => {
const jsonBuffer = Buffer.from(JSON.stringify(data)); const jsonBuffer = Buffer.from(JSON.stringify(data));
res.end(Buffer.concat([bomBuffer, jsonBuffer])); res.end(Buffer.concat([bomBuffer, jsonBuffer]));
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -261,7 +266,7 @@ describe('supports http with nodejs', () => {
res.end(expectedResponse); res.end(expectedResponse);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -283,7 +288,7 @@ describe('supports http with nodejs', () => {
res.statusCode = 302; res.statusCode = 302;
res.end(); res.end();
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -311,7 +316,7 @@ describe('supports http with nodejs', () => {
res.end(); res.end();
i++; i++;
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -333,7 +338,7 @@ describe('supports http with nodejs', () => {
res.statusCode = 302; res.statusCode = 302;
res.end(); res.end();
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -362,12 +367,12 @@ describe('supports http with nodejs', () => {
(req, res) => { (req, res) => {
requestCount += 1; requestCount += 1;
if (requestCount <= totalRedirectCount) { if (requestCount <= totalRedirectCount) {
res.setHeader('Location', 'http://localhost:8080'); res.setHeader('Location', `http://localhost:${SERVER_PORT}`);
res.writeHead(302); res.writeHead(302);
} }
res.end(); res.end();
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
const proxy = await startHTTPServer( const proxy = await startHTTPServer(
@ -392,13 +397,13 @@ describe('supports http with nodejs', () => {
res.end(); res.end();
}); });
}, },
{ port: 4000 } { port: PROXY_PORT }
); );
await axios.get(`http://localhost:${server.address().port}/`, { await axios.get(`http://localhost:${server.address().port}/`, {
proxy: { proxy: {
host: 'localhost', host: 'localhost',
port: 4000, port: PROXY_PORT,
}, },
maxRedirects: totalRedirectCount, maxRedirects: totalRedirectCount,
beforeRedirect: (options) => { beforeRedirect: (options) => {
@ -419,7 +424,7 @@ describe('supports http with nodejs', () => {
res.statusCode = 400; res.statusCode = 400;
res.end(); res.end();
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -455,7 +460,7 @@ describe('supports http with nodejs', () => {
(req, res) => { (req, res) => {
res.end(); res.end();
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -496,7 +501,7 @@ describe('supports http with nodejs', () => {
res.end(); res.end();
} }
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -532,7 +537,7 @@ describe('supports http with nodejs', () => {
res.setHeader('Content-Encoding', 'gzip'); res.setHeader('Content-Encoding', 'gzip');
res.end(zipped); res.end(zipped);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -552,7 +557,7 @@ describe('supports http with nodejs', () => {
res.setHeader('Content-Encoding', 'gzip'); res.setHeader('Content-Encoding', 'gzip');
res.end('invalid response'); res.end('invalid response');
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -584,7 +589,7 @@ describe('supports http with nodejs', () => {
res.setHeader('Content-Encoding', 'gzip'); res.setHeader('Content-Encoding', 'gzip');
res.end(zipped); res.end(zipped);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -661,10 +666,13 @@ describe('supports http with nodejs', () => {
describe(`${typeName} decompression`, () => { describe(`${typeName} decompression`, () => {
it('should support decompression', async () => { it('should support decompression', async () => {
const server = await startHTTPServer(async (req, res) => { const server = await startHTTPServer(
res.setHeader('Content-Encoding', type); async (req, res) => {
res.end(await zipped); res.setHeader('Content-Encoding', type);
}); res.end(await zipped);
},
{ port: SERVER_PORT }
);
try { try {
const { data } = await axios.get(`http://localhost:${server.address().port}`); 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 () => { it(`should not fail if response content-length header is missing (${type})`, async () => {
const server = await startHTTPServer(async (req, res) => { const server = await startHTTPServer(
res.setHeader('Content-Encoding', type); async (req, res) => {
res.removeHeader('Content-Length'); res.setHeader('Content-Encoding', type);
res.end(await zipped); res.removeHeader('Content-Length');
}); res.end(await zipped);
},
{ port: SERVER_PORT }
);
try { try {
const { data } = await axios.get(`http://localhost:${server.address().port}`); 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 () => { it('should not fail with chunked responses (without Content-Length header)', async () => {
const server = await startHTTPServer(async (req, res) => { const server = await startHTTPServer(
res.setHeader('Content-Encoding', type); async (req, res) => {
res.setHeader('Transfer-Encoding', 'chunked'); res.setHeader('Content-Encoding', type);
res.removeHeader('Content-Length'); res.setHeader('Transfer-Encoding', 'chunked');
res.write(await zipped); res.removeHeader('Content-Length');
res.end(); res.write(await zipped);
}); res.end();
},
{ port: SERVER_PORT }
);
try { try {
const { data } = await axios.get(`http://localhost:${server.address().port}`); 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 () => { it('should not fail with an empty response without content-length header (Z_BUF_ERROR)', async () => {
const server = await startHTTPServer((req, res) => { const server = await startHTTPServer(
res.setHeader('Content-Encoding', type); (req, res) => {
res.removeHeader('Content-Length'); res.setHeader('Content-Encoding', type);
res.end(); res.removeHeader('Content-Length');
}); res.end();
},
{ port: SERVER_PORT }
);
try { try {
const { data } = await axios.get(`http://localhost:${server.address().port}`); 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 () => { it('should not fail with an empty response with content-length header (Z_BUF_ERROR)', async () => {
const server = await startHTTPServer((req, res) => { const server = await startHTTPServer(
res.setHeader('Content-Encoding', type); (req, res) => {
res.end(); res.setHeader('Content-Encoding', type);
}); res.end();
},
{ port: SERVER_PORT }
);
try { try {
await axios.get(`http://localhost:${server.address().port}`); 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.setHeader('Content-Type', 'text/html; charset=UTF-8');
res.end(str); res.end(str);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -762,7 +782,7 @@ describe('supports http with nodejs', () => {
(req, res) => { (req, res) => {
res.end(req.headers.authorization); res.end(req.headers.authorization);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -783,7 +803,7 @@ describe('supports http with nodejs', () => {
(req, res) => { (req, res) => {
res.end(req.headers.authorization); res.end(req.headers.authorization);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -805,7 +825,7 @@ describe('supports http with nodejs', () => {
(req, res) => { (req, res) => {
res.end(req.headers['user-agent']); res.end(req.headers['user-agent']);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -824,7 +844,7 @@ describe('supports http with nodejs', () => {
(req, res) => { (req, res) => {
res.end(req.headers['user-agent']); res.end(req.headers['user-agent']);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -842,7 +862,7 @@ describe('supports http with nodejs', () => {
assert.strictEqual(req.headers['content-length'], '42'); assert.strictEqual(req.headers['content-length'], '42');
res.end(); res.end();
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -859,7 +879,7 @@ describe('supports http with nodejs', () => {
res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.setHeader('Content-Type', 'text/html; charset=UTF-8');
res.end(Array(5000).join('#')); res.end(Array(5000).join('#'));
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -891,7 +911,7 @@ describe('supports http with nodejs', () => {
res.statusCode = 302; res.statusCode = 302;
res.end(); res.end();
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -916,7 +936,7 @@ describe('supports http with nodejs', () => {
res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.setHeader('Content-Type', 'text/html; charset=UTF-8');
res.end(); res.end();
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -953,7 +973,7 @@ describe('supports http with nodejs', () => {
res.end('OK'); res.end('OK');
}); });
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -967,7 +987,7 @@ describe('supports http with nodejs', () => {
}); });
it('should display error while parsing params', async () => { it('should display error while parsing params', async () => {
const server = await startHTTPServer(() => {}, { port: 8080 }); const server = await startHTTPServer(() => {}, { port: SERVER_PORT });
try { try {
await assert.rejects( await assert.rejects(
@ -1044,7 +1064,7 @@ describe('supports http with nodejs', () => {
(req, res) => { (req, res) => {
req.pipe(res); req.pipe(res);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -1077,7 +1097,7 @@ describe('supports http with nodejs', () => {
}); });
it('should pass errors for a failed stream', async () => { 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'); const notExistPath = path.join(adaptersTestsDir, 'does_not_exist');
try { try {
@ -1148,7 +1168,7 @@ describe('supports http with nodejs', () => {
assert.strictEqual(req.headers['content-length'], buf.length.toString()); assert.strictEqual(req.headers['content-length'], buf.length.toString());
req.pipe(res); req.pipe(res);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -1182,7 +1202,7 @@ describe('supports http with nodejs', () => {
res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.setHeader('Content-Type', 'text/html; charset=UTF-8');
res.end('12345'); res.end('12345');
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
const proxy = await startHTTPServer( const proxy = await startHTTPServer(
@ -1207,7 +1227,7 @@ describe('supports http with nodejs', () => {
}); });
}); });
}, },
{ port: 0 } { port: PROXY_PORT }
); );
try { try {
@ -1251,9 +1271,9 @@ describe('supports http with nodejs', () => {
res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.setHeader('Content-Type', 'text/html; charset=UTF-8');
res.end('12345'); res.end('12345');
}, },
{ port: 8080 } { port: SERVER_PORT }
) )
.listen(8080, () => resolve(httpsServer)); .listen(SERVER_PORT, () => resolve(httpsServer));
httpsServer.on('error', reject); httpsServer.on('error', reject);
}); });
@ -1290,9 +1310,9 @@ describe('supports http with nodejs', () => {
response.end(); response.end();
}); });
}, },
{ port: 8081 } { port: PROXY_PORT }
) )
.listen(8081, () => resolve(httpsProxy)); .listen(PROXY_PORT, () => resolve(httpsProxy));
httpsProxy.on('error', reject); httpsProxy.on('error', reject);
}); });
@ -1324,7 +1344,7 @@ describe('supports http with nodejs', () => {
res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.setHeader('Content-Type', 'text/html; charset=UTF-8');
res.end('123456789'); res.end('123456789');
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -1355,7 +1375,7 @@ describe('supports http with nodejs', () => {
res.setHeader('Content-Type', 'text/html; charset=UTF-8'); res.setHeader('Content-Type', 'text/html; charset=UTF-8');
res.end('4567'); res.end('4567');
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
const proxy = await startHTTPServer( 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}/`; 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.setHeader('Content-Type', 'text/html; charset=UTF-8');
res.end('12345'); res.end('12345');
}, },
{ port: 8080 } { port: SERVER_PORT }
) )
.listen(8080, () => resolve(httpsServer)); .listen(SERVER_PORT, () => resolve(httpsServer));
httpsServer.on('error', reject); httpsServer.on('error', reject);
}); });
@ -1497,9 +1517,9 @@ describe('supports http with nodejs', () => {
response.end(); response.end();
}); });
}, },
{ port: 8081 } { port: PROXY_PORT }
) )
.listen(8081, () => resolve(httpsProxy)); .listen(PROXY_PORT, () => resolve(httpsProxy));
httpsProxy.on('error', reject); httpsProxy.on('error', reject);
}); });
@ -1561,7 +1581,7 @@ describe('supports http with nodejs', () => {
res.statusCode = 302; res.statusCode = 302;
res.end(); res.end();
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
const proxy = await startHTTPServer( 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}`; 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.setHeader('Content-Type', 'text/html; charset=UTF-8');
res.end('4567'); res.end('4567');
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
const proxy = await startHTTPServer( 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'; 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.setHeader('Content-Type', 'text/html; charset=UTF-8');
res.end('4567'); res.end('4567');
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
const proxy = await startHTTPServer( 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'; const noProxyValue = 'foo.com, ,bar.net , quix.co';
@ -1803,7 +1823,7 @@ describe('supports http with nodejs', () => {
(req, res) => { (req, res) => {
res.end(); res.end();
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
const proxy = await startHTTPServer( const proxy = await startHTTPServer(
@ -1825,7 +1845,7 @@ describe('supports http with nodejs', () => {
}); });
}); });
}, },
{ port: 8081 } { port: PROXY_PORT }
); );
try { try {
@ -1858,7 +1878,7 @@ describe('supports http with nodejs', () => {
(req, res) => { (req, res) => {
res.end(); res.end();
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
const proxy = await startHTTPServer( 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}/`; const proxyUrl = `http://user:pass@localhost:${proxy.address().port}/`;
@ -1932,7 +1952,7 @@ describe('supports http with nodejs', () => {
const proxy = { const proxy = {
protocol: 'http:', protocol: 'http:',
host: 'hostname.abc.xyz', host: 'hostname.abc.xyz',
port: 3300, port: PROXY_PORT,
auth: { auth: {
username: '', username: '',
password: '', password: '',
@ -1954,23 +1974,48 @@ describe('supports http with nodejs', () => {
const testCases = [ const testCases = [
{ {
description: 'hostname and trailing colon in protocol', description: 'hostname and trailing colon in protocol',
proxyConfig: { hostname: '127.0.0.1', protocol: 'http:', port: 80 }, proxyConfig: { hostname: '127.0.0.1', protocol: 'http:', port: OPEN_WEB_PORT },
expectedOptions: { host: '127.0.0.1', protocol: 'http:', port: 80, path: destination }, expectedOptions: {
host: '127.0.0.1',
protocol: 'http:',
port: OPEN_WEB_PORT,
path: destination,
},
}, },
{ {
description: 'hostname and no trailing colon in protocol', description: 'hostname and no trailing colon in protocol',
proxyConfig: { hostname: '127.0.0.1', protocol: 'http', port: 80 }, proxyConfig: { hostname: '127.0.0.1', protocol: 'http', port: OPEN_WEB_PORT },
expectedOptions: { host: '127.0.0.1', protocol: 'http:', port: 80, path: destination }, expectedOptions: {
host: '127.0.0.1',
protocol: 'http:',
port: OPEN_WEB_PORT,
path: destination,
},
}, },
{ {
description: 'both hostname and host -> hostname takes precedence', description: 'both hostname and host -> hostname takes precedence',
proxyConfig: { hostname: '127.0.0.1', host: '0.0.0.0', protocol: 'http', port: 80 }, proxyConfig: {
expectedOptions: { host: '127.0.0.1', protocol: 'http:', port: 80, path: destination }, 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', description: 'only host and https protocol',
proxyConfig: { host: '0.0.0.0', protocol: 'https', port: 80 }, proxyConfig: { host: '0.0.0.0', protocol: 'https', port: OPEN_WEB_PORT },
expectedOptions: { host: '0.0.0.0', protocol: 'https:', port: 80, path: destination }, 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. // Call cancel() when the request has been sent but no response received.
source.cancel('Operation has been canceled.'); source.cancel('Operation has been canceled.');
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -2024,7 +2069,7 @@ describe('supports http with nodejs', () => {
(req, res) => { (req, res) => {
res.end(); res.end();
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -2046,7 +2091,7 @@ describe('supports http with nodejs', () => {
res.end(); res.end();
}, 1000); }, 1000);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -2072,9 +2117,9 @@ describe('supports http with nodejs', () => {
res.end(); res.end();
}, 1000); }, 1000);
}, },
{ port: 8080 } { port: SERVER_PORT }
) )
.listen(8080, () => resolve(httpsServer)); .listen(SERVER_PORT, () => resolve(httpsServer));
httpsServer.on('error', reject); httpsServer.on('error', reject);
}); });
@ -2120,7 +2165,7 @@ describe('supports http with nodejs', () => {
assert.equal(req.headers['user-agent'], `axios/${axios.VERSION}`); assert.equal(req.headers['user-agent'], `axios/${axios.VERSION}`);
res.end(); res.end();
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -2137,7 +2182,7 @@ describe('supports http with nodejs', () => {
assert.equal('User-Agent' in req.headers, false); assert.equal('User-Agent' in req.headers, false);
res.end(); res.end();
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -2165,7 +2210,7 @@ describe('supports http with nodejs', () => {
res.destroy(); res.destroy();
}, 200); }, 200);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -2190,7 +2235,7 @@ describe('supports http with nodejs', () => {
(req, res) => { (req, res) => {
res.end('ok'); res.end('ok');
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -2259,7 +2304,7 @@ describe('supports http with nodejs', () => {
); );
}); });
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -2297,7 +2342,7 @@ describe('supports http with nodejs', () => {
}) })
); );
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -2355,7 +2400,7 @@ describe('supports http with nodejs', () => {
const expressServer = app.listen(0, () => resolve(expressServer)); const expressServer = app.listen(0, () => resolve(expressServer));
expressServer.on('error', reject); expressServer.on('error', reject);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -2391,7 +2436,7 @@ describe('supports http with nodejs', () => {
async (req, res) => { async (req, res) => {
res.end(await getStream(req)); res.end(await getStream(req));
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -2433,7 +2478,7 @@ describe('supports http with nodejs', () => {
const expressServer = app.listen(0, () => resolve(expressServer)); const expressServer = app.listen(0, () => resolve(expressServer));
expressServer.on('error', reject); expressServer.on('error', reject);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -2475,7 +2520,7 @@ describe('supports http with nodejs', () => {
(req, res) => { (req, res) => {
req.pipe(res); req.pipe(res);
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -2541,7 +2586,7 @@ describe('supports http with nodejs', () => {
{ {
rate: 100 * 1024, rate: 100 * 1024,
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -2610,7 +2655,7 @@ describe('supports http with nodejs', () => {
{ {
rate: 100 * 1024, rate: 100 * 1024,
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {
@ -2783,10 +2828,13 @@ describe('supports http with nodejs', () => {
describe('request aborting', () => { describe('request aborting', () => {
it('should be able to abort the response stream', async () => { it('should be able to abort the response stream', async () => {
const server = await startHTTPServer({ const server = await startHTTPServer(
rate: 100000, {
useBuffering: true, rate: 100000,
}); useBuffering: true,
},
{ port: SERVER_PORT }
);
try { try {
const buf = Buffer.alloc(1024 * 1024); const buf = Buffer.alloc(1024 * 1024);
@ -2832,7 +2880,7 @@ describe('supports http with nodejs', () => {
}); });
it('should support function as paramsSerializer value', async () => { 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 { try {
const { data } = await axios.post(`http://localhost:${server.address().port}`, 'test', { 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 { try {
@ -3043,7 +3091,7 @@ describe('supports http with nodejs', () => {
}, },
{ {
useHTTP2: true, useHTTP2: true,
port: 8080, port: SERVER_PORT,
} }
); );
@ -3060,7 +3108,7 @@ describe('supports http with nodejs', () => {
it('should support request payload', async () => { it('should support request payload', async () => {
const server = await startHTTPServer(null, { const server = await startHTTPServer(null, {
useHTTP2: true, useHTTP2: true,
port: 8080, port: SERVER_PORT,
}); });
try { try {
@ -3092,7 +3140,7 @@ describe('supports http with nodejs', () => {
}, },
{ {
useHTTP2: true, useHTTP2: true,
port: 8080, port: SERVER_PORT,
} }
); );
@ -3134,7 +3182,7 @@ describe('supports http with nodejs', () => {
}, },
{ {
useHTTP2: true, useHTTP2: true,
port: 8080, port: SERVER_PORT,
} }
); );
@ -3165,7 +3213,7 @@ describe('supports http with nodejs', () => {
}, },
{ {
useHTTP2: true, useHTTP2: true,
port: 8080, port: SERVER_PORT,
} }
); );
@ -3210,7 +3258,7 @@ describe('supports http with nodejs', () => {
}, },
{ {
useHTTP2: true, useHTTP2: true,
port: 8080, port: SERVER_PORT,
} }
); );
@ -3251,7 +3299,7 @@ describe('supports http with nodejs', () => {
}, },
{ {
useHTTP2: true, useHTTP2: true,
port: 8080, port: SERVER_PORT,
} }
); );
@ -3302,7 +3350,7 @@ describe('supports http with nodejs', () => {
}, },
{ {
useHTTP2: true, useHTTP2: true,
port: 8080, port: SERVER_PORT,
} }
); );
@ -3339,7 +3387,7 @@ describe('supports http with nodejs', () => {
}, },
{ {
useHTTP2: true, useHTTP2: true,
port: 8080, port: SERVER_PORT,
} }
); );
@ -3351,7 +3399,7 @@ describe('supports http with nodejs', () => {
}, },
{ {
useHTTP2: true, useHTTP2: true,
port: 8081, port: ALTERNATE_SERVER_PORT,
} }
); );
@ -3389,7 +3437,7 @@ describe('supports http with nodejs', () => {
}, },
{ {
useHTTP2: true, useHTTP2: true,
port: 8080, port: SERVER_PORT,
} }
); );
@ -3428,7 +3476,7 @@ describe('supports http with nodejs', () => {
}, },
{ {
useHTTP2: true, useHTTP2: true,
port: 8080, port: SERVER_PORT,
} }
); );
@ -3470,7 +3518,7 @@ describe('supports http with nodejs', () => {
}, },
{ {
useHTTP2: true, useHTTP2: true,
port: 8080, port: SERVER_PORT,
} }
); );
@ -3542,7 +3590,7 @@ describe('supports http with nodejs', () => {
res.end('ok'); res.end('ok');
}, },
{ port: 8080 } { port: SERVER_PORT }
); );
try { try {

97
tests/unit/api.test.js Normal file
View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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/');
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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();
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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,
});
});
});

View File

@ -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]);
});
});

View File

@ -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 });
});
});

View File

@ -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');
});
});

View File

@ -25,6 +25,22 @@ export default defineConfig({
setupFiles: ['tests/setup/browser.setup.js'], 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'],
},
},
], ],
}, },
}); });