2
0
mirror of https://github.com/tenrok/axios.git synced 2026-06-17 19:21:29 +03:00

Update unit testing flows as part of migration to vitest (#7484)

* chore: small fixes to tests

* feat: transitional move to vitests

* feat: moving unit tests in progress

* feat: moving more unit tests over

* feat: more tests moved

* feat: updated more sections of the http test

* chore: wip http tests

* chore: wip http tests

* chore: more http tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: tests

* chore: remove un-needed docs

* chore: update package lock

* chore: update lock
This commit is contained in:
Jay
2026-03-06 20:42:14 +02:00
committed by GitHub
parent 84285c8f63
commit fa337332b9
90 changed files with 9633 additions and 2862 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: npm
- name: Install dependencies
- name: Install dependencies Node 14
if: matrix.node-version == '14.x'
run: npm i
- name: Install dependencies
+3421 -2826
View File
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -49,6 +49,10 @@
"test:node": "npm run test:mocha",
"test:node:coverage": "c8 npm run test:mocha",
"test:browser": "npm run test:karma",
"test:vitest": "vitest run",
"test:vitest:unit": "vitest run --project unit",
"test:vitest:browser": "vitest run --project browser",
"test:vitest:watch": "vitest",
"test:package": "npm run test:eslint && npm run test:exports",
"test:eslint": "node bin/ssl_hotfix.js eslint lib/**/*.js",
"test:mocha": "node bin/ssl_hotfix.js mocha test/unit/**/*.js --timeout 30000 --exit",
@@ -100,6 +104,8 @@
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-multi-entry": "^4.1.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@vitest/browser": "^4.0.18",
"@vitest/browser-playwright": "^4.0.18",
"abortcontroller-polyfill": "^1.7.8",
"auto-changelog": "^2.5.0",
"body-parser": "^1.20.4",
@@ -136,6 +142,7 @@
"mocha": "^10.8.2",
"multer": "^1.4.4",
"pacote": "^20.0.0",
"playwright": "^1.58.2",
"prettier": "^3.8.1",
"pretty-bytes": "^6.1.1",
"rollup": "^2.79.2",
@@ -147,7 +154,8 @@
"stream-throttle": "^0.1.3",
"string-replace-async": "^3.0.2",
"tar-stream": "^3.1.7",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"vitest": "^4.0.18"
},
"browser": {
"./dist/node/axios.cjs": "./dist/browser/axios.cjs",
+10 -17
View File
@@ -75,21 +75,6 @@ export default async () => {
banner,
},
}),
// browser ESM bundle for CDN with fetch adapter only
// Downsizing from 12.97 kB (gzip) to 12.23 kB (gzip)
/* ...buildConfig({
input: namedInput,
output: {
file: `dist/esm/${outputFileName}-fetch.js`,
format: "esm",
preferConst: true,
exports: "named",
banner
},
alias: [
{ find: './xhr.js', replacement: '../helpers/null.js' }
]
}),*/
// Browser UMD bundle for CDN
...buildConfig({
@@ -118,7 +103,7 @@ export default async () => {
},
}),
// Node.js commonjs bundle
// Node.js commonjs bundle (transpiled for Node 12)
{
input: defaultInput,
output: {
@@ -128,7 +113,15 @@ export default async () => {
exports: 'default',
banner,
},
plugins: [autoExternal(), resolve(), commonjs()],
plugins: [
autoExternal(),
resolve(),
commonjs(),
babel({
babelHelpers: 'bundled',
presets: [['@babel/preset-env', { targets: { node: '12' } }]],
}),
],
},
];
};
+2
View File
@@ -1,3 +1,5 @@
/* eslint-env mocha */
/* global jasmine */
import _axios from '../../index.js';
window.axios = _axios;
+2
View File
@@ -1,3 +1,5 @@
/* eslint-env mocha */
/* global jasmine */
describe('adapter', function () {
beforeEach(function () {
jasmine.Ajax.install();
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
describe('static api', function () {
it('should have request method helpers', function () {
expect(typeof axios.request).toEqual('function');
+2
View File
@@ -1,3 +1,5 @@
/* eslint-env mocha */
/* global jasmine */
import axios from '../../index';
function validateInvalidCharacterError(error) {
+2
View File
@@ -1,3 +1,5 @@
/* eslint-env mocha */
/* global jasmine */
const Cancel = axios.Cancel;
const CancelToken = axios.CancelToken;
import { AbortController as _AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill.js';
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import CancelToken from '../../../lib/cancel/CancelToken';
import CanceledError from '../../../lib/cancel/CanceledError';
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import CanceledError from '../../../lib/cancel/CanceledError';
describe('Cancel', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import isCancel from '../../../lib/cancel/isCancel';
import CanceledError from '../../../lib/cancel/CanceledError';
+16 -15
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import AxiosError from '../../../lib/core/AxiosError';
describe('core::AxiosError', function () {
@@ -86,24 +87,24 @@ describe('core::AxiosError', function () {
});
it('should have message property as enumerable for backward compatibility', () => {
const err = new AxiosError('Test error message', 'ERR_TEST', {foo: 'bar'});
const err = new AxiosError('Test error message', 'ERR_TEST', { foo: 'bar' });
// Test Object.keys() includes message
const keys = Object.keys(err);
expect(keys).toContain('message');
// Test Object.keys() includes message
const keys = Object.keys(err);
expect(keys).toContain('message');
// Test Object.entries() includes message
const entries = Object.entries(err);
const messageEntry = entries.find(([key]) => key === 'message');
expect(messageEntry).toBeDefined();
expect(messageEntry[1]).toBe('Test error message');
// Test Object.entries() includes message
const entries = Object.entries(err);
const messageEntry = entries.find(([key]) => key === 'message');
expect(messageEntry).toBeDefined();
expect(messageEntry[1]).toBe('Test error message');
// Test spread operator includes message
const spread = {...err};
expect(spread.message).toBe('Test error message');
// Test spread operator includes message
const spread = { ...err };
expect(spread.message).toBe('Test error message');
// Verify message descriptor is enumerable
const descriptor = Object.getOwnPropertyDescriptor(err, 'message');
expect(descriptor.enumerable).toBe(true);
// Verify message descriptor is enumerable
const descriptor = Object.getOwnPropertyDescriptor(err, 'message');
expect(descriptor.enumerable).toBe(true);
});
});
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import buildFullPath from '../../../lib/core/buildFullPath';
describe('helpers::buildFullPath', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import defaults from '../../../lib/defaults';
import mergeConfig from '../../../lib/core/mergeConfig';
import { AxiosHeaders } from '../../../index.js';
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import settle from '../../../lib/core/settle';
describe('core::settle', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import transformData from '../../../lib/core/transformData';
describe('core::transformData', function () {
+2
View File
@@ -1,3 +1,5 @@
/* eslint-env mocha */
/* global jasmine */
import defaults from '../../lib/defaults';
import AxiosHeaders from '../../lib/core/AxiosHeaders';
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import { retryNetwork } from '../helpers/retry.js';
describe('FormData', function () {
+2
View File
@@ -1,3 +1,5 @@
/* eslint-env mocha */
/* global jasmine */
const { AxiosHeaders } = axios;
function testHeaderValue(headers, key, val) {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import bind from '../../../lib/helpers/bind';
describe('bind', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import buildURL from '../../../lib/helpers/buildURL';
describe('helpers::buildURL', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import combineURLs from '../../../lib/helpers/combineURLs';
describe('helpers::combineURLs', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import cookies from '../../../lib/helpers/cookies';
describe('helpers::cookies', function () {
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import formDataToJSON from '../../../lib/helpers/formDataToJSON';
describe('formDataToJSON', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import isAbsoluteURL from '../../../lib/helpers/isAbsoluteURL';
describe('helpers::isAbsoluteURL', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import AxiosError from '../../../lib/core/AxiosError';
import isAxiosError from '../../../lib/helpers/isAxiosError';
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import isURLSameOrigin from '../../../lib/helpers/isURLSameOrigin';
describe('helpers::isURLSameOrigin', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import parseHeaders from '../../../lib/helpers/parseHeaders';
describe('helpers::parseHeaders', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import spread from '../../../lib/helpers/spread';
describe('helpers::spread', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import toFormData from '../../../lib/helpers/toFormData';
describe('toFormData', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
'use strict';
import validator from '../../../lib/helpers/validator';
+2
View File
@@ -1,3 +1,5 @@
/* eslint-env mocha */
/* global jasmine */
describe('instance', function () {
beforeEach(function () {
jasmine.Ajax.install();
+2
View File
@@ -1,3 +1,5 @@
/* eslint-env mocha */
/* global jasmine */
describe('interceptors', function () {
beforeEach(function () {
jasmine.Ajax.install();
+3
View File
@@ -1,3 +1,6 @@
/* eslint-env mocha */
/* global jasmine */
// import AxiosHeaders from "../../lib/core/AxiosHeaders.js";
// import isAbsoluteURL from '../../lib/helpers/isAbsoluteURL.js';
+2
View File
@@ -1,3 +1,5 @@
/* eslint-env mocha */
/* global jasmine */
describe('progress events', function () {
beforeEach(function () {
jasmine.Ajax.install();
+2
View File
@@ -1,3 +1,5 @@
/* eslint-env mocha */
/* global jasmine */
describe('promise', function () {
beforeEach(function () {
jasmine.Ajax.install();
+2
View File
@@ -1,3 +1,5 @@
/* eslint-env mocha */
/* global jasmine */
describe('requests', function () {
beforeEach(function () {
jasmine.Ajax.install();
+2
View File
@@ -1,3 +1,5 @@
/* eslint-env mocha */
/* global jasmine */
import AxiosError from '../../lib/core/AxiosError';
describe('transform', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import utils from '../../../lib/utils';
const { kindOf } = utils;
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import utils from '../../../lib/utils';
const { extend } = utils;
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import utils from '../../../lib/utils';
const { forEach } = utils;
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import utils from '../../../lib/utils';
describe('utils::isX', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import utils from '../../../lib/utils';
const { kindOf } = utils;
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import { kindOfTest } from '../../../lib/utils';
describe('utils::kindOfTest', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import utils from '../../../lib/utils';
const { merge } = utils;
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import utils from '../../../lib/utils';
const { toArray } = utils;
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import utils from '../../../lib/utils';
const { toFlatObject } = utils;
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import utils from '../../../lib/utils';
describe('utils::trim', function () {
+2
View File
@@ -1,3 +1,5 @@
/* eslint-env mocha */
/* global jasmine */
import cookies from '../../lib/helpers/cookies';
describe('xsrf', function () {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import adapters from '../../../lib/adapters/adapters.js';
import assert from 'assert';
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import assert from 'assert';
import {
startHTTPServer,
+1 -2
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import axios from '../../../index.js';
import http from 'http';
import https from 'https';
@@ -2464,8 +2465,6 @@ describe('supports http with nodejs', function () {
});
describe('request aborting', function () {
//this.timeout(5000);
it('should be able to abort the response stream', async () => {
server = await startHTTPServer({
rate: 100_000,
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import Axios from '../../../lib/core/Axios.js';
import assert from 'assert';
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import AxiosHeaders from '../../../lib/core/AxiosHeaders.js';
import assert from 'assert';
+2
View File
@@ -1,3 +1,5 @@
/* eslint-disable no-prototype-builtins */
/* eslint-env mocha */
'use strict';
import assert from 'assert';
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import defaults from '../../../lib/defaults/index.js';
import transformData from '../../../lib/core/transformData.js';
import assert from 'assert';
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import assert from 'assert';
import composeSignals from '../../../lib/helpers/composeSignals.js';
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import assert from 'assert';
import estimateDataURLDecodedBytes from '../../../lib/helpers/estimateDataURLDecodedBytes.js';
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import assert from 'assert';
import fromDataURI from '../../../lib/helpers/fromDataURI.js';
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import assert from 'assert';
import utils from '../../../lib/utils.js';
import parseProtocol from '../../../lib/helpers/parseProtocol.js';
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import assert from 'assert';
import toFormData from '../../../lib/helpers/toFormData.js';
import FormData from 'form-data';
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import platform from '../../../lib/platform/index.js';
import assert from 'assert';
@@ -1,3 +1,4 @@
/* eslint-env mocha */
// https://snyk.io/vuln/SNYK-JS-AXIOS-1038255
// https://github.com/axios/axios/issues/3407
// https://github.com/axios/axios/issues/3369
@@ -1,3 +1,4 @@
/* eslint-env mocha */
// https://security.snyk.io/vuln/SNYK-JS-AXIOS-7361793
// https://github.com/axios/axios/issues/6463
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import assert from 'assert';
import http from 'http';
import axios from '../../../index.js';
+1
View File
@@ -1,3 +1,4 @@
/* eslint-env mocha */
import assert from 'assert';
import utils from '../../../lib/utils.js';
import FormData from 'form-data';
+10
View File
@@ -0,0 +1,10 @@
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();
});
+5
View File
@@ -0,0 +1,5 @@
import { afterEach } from 'vitest';
afterEach(() => {
document.body.innerHTML = '';
});
+252
View File
@@ -0,0 +1,252 @@
import http from 'http';
import http2 from 'http2';
import stream from 'stream';
import getStream from 'get-stream';
import { Throttle } from 'stream-throttle';
import formidable from 'formidable';
import selfsigned from 'selfsigned';
export const LOCAL_SERVER_URL = 'http://localhost:4444';
export const SERVER_HANDLER_STREAM_ECHO = (req, res) => req.pipe(res);
export const setTimeoutAsync = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const certificate = selfsigned.generate(null, { keySize: 2048 });
const trackedServers = new Set();
const untrackServer = (server) => {
trackedServers.delete(server);
};
export const startHTTPServer = (handlerOrOptions, options) => {
const {
handler,
useBuffering = false,
rate = undefined,
port = 4444,
keepAlive = 1000,
useHTTP2,
key = certificate.private,
cert = certificate.cert,
} = Object.assign(
typeof handlerOrOptions === 'function'
? {
handler: handlerOrOptions,
}
: handlerOrOptions || {},
options
);
return new Promise((resolve, reject) => {
const serverHandler =
handler ||
async function (req, res) {
try {
req.headers['content-length'] &&
res.setHeader('content-length', req.headers['content-length']);
let dataStream = req;
if (useBuffering) {
dataStream = stream.Readable.from(await getStream(req));
}
const streams = [dataStream];
if (rate) {
streams.push(new Throttle({ rate }));
}
streams.push(res);
stream.pipeline(streams, (err) => {
err && console.log('Server warning: ' + err.message);
});
} catch (err) {
console.warn('HTTP server error:', err);
}
};
const server = useHTTP2
? http2.createSecureServer({ key, cert }, serverHandler)
: http.createServer(serverHandler);
const sessions = new Set();
if (useHTTP2) {
server.on('session', (session) => {
sessions.add(session);
session.once('close', () => {
sessions.delete(session);
});
});
server.closeAllSessions = () => {
for (const session of sessions) {
session.destroy();
}
};
} else {
server.keepAliveTimeout = keepAlive;
}
server.listen(port, function (err) {
if (err) {
reject(err);
return;
}
trackedServers.add(this);
resolve(this);
});
});
};
export const stopHTTPServer = async (server, timeout = 10000) => {
if (server) {
if (typeof server.closeAllConnections === 'function') {
server.closeAllConnections();
}
if (typeof server.closeAllSessions === 'function') {
server.closeAllSessions();
}
await Promise.race([new Promise((resolve) => server.close(resolve)), setTimeoutAsync(timeout)]);
untrackServer(server);
}
};
export const stopAllTrackedHTTPServers = async (timeout = 10000) => {
const servers = Array.from(trackedServers);
await Promise.all(servers.map((server) => stopHTTPServer(server, timeout)));
};
export const handleFormData = (req) => {
return new Promise((resolve, reject) => {
const form = new formidable.IncomingForm();
form.parse(req, (err, fields, files) => {
if (err) {
return reject(err);
}
resolve({ fields, files });
});
});
};
export const nodeVersion = process.versions.node.split('.').map((v) => parseInt(v, 10));
export const generateReadable = (length = 1024 * 1024, chunkSize = 10 * 1024, sleep = 50) => {
return stream.Readable.from(
(async function* () {
let dataLength = 0;
while (dataLength < length) {
const leftBytes = length - dataLength;
const chunk = Buffer.alloc(leftBytes > chunkSize ? chunkSize : leftBytes);
dataLength += chunk.length;
yield chunk;
if (sleep) {
await setTimeoutAsync(sleep);
}
}
})()
);
};
export const makeReadableStream = (chunk = 'chunk', n = 10, timeout = 100) => {
return new ReadableStream(
{
async pull(controller) {
await setTimeoutAsync(timeout);
n-- ? controller.enqueue(chunk) : controller.close();
},
},
{
highWaterMark: 1,
}
);
};
export const makeEchoStream = (echo) =>
new WritableStream({
write(chunk) {
echo && console.log('Echo chunk', chunk);
},
});
export const startTestServer = async (port) => {
const handler = async (req) => {
const parsed = new URL(req.url, `http://localhost:${port}`);
const params = Object.fromEntries(parsed.searchParams);
const response = {
url: req.url,
pathname: parsed.pathname,
params,
method: req.method,
headers: req.headers,
};
const contentType = req.headers['content-type'] || '';
const { delay = 0 } = params;
if (+delay) {
await setTimeoutAsync(+delay);
}
switch (parsed.pathname.replace(/\/$/, '')) {
case '/echo/json':
default:
if (contentType.startsWith('multipart/')) {
const { fields, files } = await handleFormData(req);
response.form = fields;
response.files = files;
} else {
response.body = (await getStream(req, { encoding: 'buffer' })).toString('hex');
}
return {
body: response,
};
}
};
return await startHTTPServer(
(req, res) => {
res.setHeader('Access-Control-Allow-Origin', `*`);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', '*');
res.setHeader('Access-Control-Max-Age', '86400');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
Promise.resolve(handler(req, res)).then((result) => {
const { status = 200, headers = {}, body } = result || {};
res.statusCode = status;
Object.entries(headers).forEach(([header, value]) => {
res.setHeader(header, value);
});
res.end(JSON.stringify(body, null, 2));
});
},
{ port }
);
};
+48
View File
@@ -0,0 +1,48 @@
import { beforeEach, describe, it } from 'vitest';
import assert from 'assert';
import adapters from '../../../lib/adapters/adapters.js';
describe('adapters', () => {
const store = { ...adapters.adapters };
beforeEach(() => {
Object.keys(adapters.adapters).forEach((name) => {
delete adapters.adapters[name];
});
Object.assign(adapters.adapters, store);
});
it('should support loading by fn handle', () => {
const adapter = () => {};
assert.strictEqual(adapters.getAdapter(adapter), adapter);
});
it('should support loading by name', () => {
const adapter = () => {};
adapters.adapters.testadapter = adapter;
assert.strictEqual(adapters.getAdapter('testAdapter'), adapter);
});
it('should detect adapter unavailable status', () => {
adapters.adapters.testadapter = null;
assert.throws(() => adapters.getAdapter('testAdapter'), /is not available in the build/);
});
it('should detect adapter unsupported status', () => {
adapters.adapters.testadapter = false;
assert.throws(() => adapters.getAdapter('testAdapter'), /is not supported by the environment/);
});
it('should pick suitable adapter from the list', () => {
const adapter = () => {};
Object.assign(adapters.adapters, {
foo: false,
bar: null,
baz: adapter,
});
assert.strictEqual(adapters.getAdapter(['foo', 'bar', 'baz']), adapter);
});
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+17
View File
@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICpDCCAYwCCQDbqELLwgbPdDANBgkqhkiG9w0BAQUFADAUMRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMjAwNjI2MjIxMTQ3WhcNNDcxMTExMjIxMTQ3WjAUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD6
Ogt99/dZ0UgbCuVV1RZ9n28Ov3DzrJCkjperQoXomIq3Fr4RUI1a2rwe3mtl3UzE
1IVZVvWPGdEsEQHwXfAsP/jFGTwI3HDyOhcqzFQSKsjvqJWYkOOb+2r3SBrFlRZW
09k/3lC+hx2XtuuG68u4Xgn3AlUvm2vplgCN7eiYcGeNwVuf2eHdOqTRTqiYCZLi
T8GtdYMDXOrwsGZs/jUKd9U0ar/lqwMhmw07yzlVDM2MWM2tyq/asQ7Sf7vuoMFu
oAtDJ3E+bK1k/7SNhdyP4RonhyUCkWG+mzoKDS1qgXroTiQSDUksAvOCTcj8BNIT
ee+Lcn9FaTKNJiKiU9q/AgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAFi5ZpaUj+mU
dsgOka+j2/njgNXux3cOjhm7z/N7LeTuDENAOrYa5b+j5JX/YM7RKHrkbXHsQbfs
GB3ufH6QhSiCd/AdsXp/TbCE/8gdq8ykkjwVP1bvBle9oPH7x1aO/WP/odsepYUv
o9aOZW4iNQVmwamU62ezglf3QD7HPeE4LnZueaFtuzRoC+aWT9v0MIeUPJLe3WDQ
FEySwUuthMDJEv92/TeK0YOiunmseCu2mvdiDj6E3C9xa5q2DWgl+msu7+bPgvYO
GuWaoNeQQGk7ebBO3Hk3IyaGx6Cbd8ty+YaZW7dUT+m7KCs1VkxdcDMjZJVWiJy4
4HcEcKboG4Y=
-----END CERTIFICATE-----
+83
View File
@@ -0,0 +1,83 @@
import { describe, it } from 'vitest';
import assert from 'assert';
import https from 'https';
import net from 'net';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import axios from '../../../index.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const getClosedPort = async () => {
return await new Promise((resolve) => {
const srv = net.createServer();
srv.listen(0, '127.0.0.1', () => {
const { port } = srv.address();
srv.close(() => resolve(port));
});
});
};
describe('adapters - network-error details', () => {
it('should expose ECONNREFUSED and set error.cause on connection refusal', async () => {
const port = await getClosedPort();
try {
await axios.get(`http://127.0.0.1:${port}`, { timeout: 500 });
assert.fail('request unexpectedly succeeded');
} catch (err) {
assert.ok(err instanceof Error, 'should be an Error');
assert.strictEqual(err.isAxiosError, true, 'isAxiosError should be true');
assert.strictEqual(err.code, 'ECONNREFUSED');
assert.ok('cause' in err, 'error.cause should exist');
assert.ok(err.cause instanceof Error, 'cause should be an Error');
assert.strictEqual(err.cause && err.cause.code, 'ECONNREFUSED');
assert.strictEqual(typeof err.message, 'string');
}
});
it('should expose self-signed TLS error and set error.cause', async () => {
const certsDir = path.resolve(__dirname, '../../../tests/unit/adapters/');
const keyPath = path.join(certsDir, 'key.pem');
const certPath = path.join(certsDir, 'cert.pem');
const key = fs.readFileSync(keyPath);
const cert = fs.readFileSync(certPath);
const httpsServer = https.createServer({ key, cert }, (req, res) => res.end('ok'));
await new Promise((resolve) => httpsServer.listen(0, '127.0.0.1', resolve));
const { port } = httpsServer.address();
try {
await axios.get(`https://127.0.0.1:${port}`, {
timeout: 500,
httpsAgent: new https.Agent({ rejectUnauthorized: true }),
});
assert.fail('request unexpectedly succeeded');
} catch (err) {
const codeStr = String(err.code);
assert.ok(
/SELF_SIGNED|UNABLE_TO_VERIFY_LEAF_SIGNATURE|DEPTH_ZERO/.test(codeStr),
`unexpected TLS code: ${codeStr}`
);
assert.ok('cause' in err, 'error.cause should exist');
assert.ok(err.cause instanceof Error, 'cause should be an Error');
const causeCode = String(err.cause && err.cause.code);
assert.ok(
/SELF_SIGNED|UNABLE_TO_VERIFY_LEAF_SIGNATURE|DEPTH_ZERO/.test(causeCode),
`unexpected cause code: ${causeCode}`
);
assert.strictEqual(typeof err.message, 'string');
} finally {
await new Promise((resolve) => httpsServer.close(resolve));
}
});
});
+531
View File
@@ -0,0 +1,531 @@
import { afterEach, describe, it, vi } from 'vitest';
import assert from 'assert';
import {
startHTTPServer,
stopHTTPServer,
LOCAL_SERVER_URL,
setTimeoutAsync,
makeReadableStream,
generateReadable,
makeEchoStream,
} from '../../setup/server.js';
import axios from '../../../index.js';
import stream from 'stream';
import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill.js';
import util from 'util';
import NodeFormData from 'form-data';
const pipelineAsync = util.promisify(stream.pipeline);
const fetchAxios = axios.create({
baseURL: LOCAL_SERVER_URL,
adapter: 'fetch',
});
let server;
describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => {
afterEach(async () => {
await stopHTTPServer(server);
server = null;
});
describe('responses', () => {
it('should support text response type', async () => {
const originalData = 'my data';
server = await startHTTPServer((req, res) => res.end(originalData));
const { data } = await fetchAxios.get('/', {
responseType: 'text',
});
assert.deepStrictEqual(data, originalData);
});
it('should support arraybuffer response type', async () => {
const originalData = 'my data';
server = await startHTTPServer((req, res) => res.end(originalData));
const { data } = await fetchAxios.get('/', {
responseType: 'arraybuffer',
});
assert.deepStrictEqual(
data,
Uint8Array.from(await new TextEncoder().encode(originalData)).buffer
);
});
it('should support blob response type', async () => {
const originalData = 'my data';
server = await startHTTPServer((req, res) => res.end(originalData));
const { data } = await fetchAxios.get('/', {
responseType: 'blob',
});
assert.deepStrictEqual(data, new Blob([originalData]));
});
it('should support stream response type', async () => {
const originalData = 'my data';
server = await startHTTPServer((req, res) => res.end(originalData));
const { data } = await fetchAxios.get('/', {
responseType: 'stream',
});
assert.ok(data instanceof ReadableStream, 'data is not instanceof ReadableStream');
const response = new Response(data);
assert.deepStrictEqual(await response.text(), originalData);
});
it('should support formData response type', async () => {
const originalData = new FormData();
originalData.append('x', '123');
server = await startHTTPServer(async (req, res) => {
const response = await new Response(originalData);
res.setHeader('Content-Type', response.headers.get('Content-Type'));
res.end(await response.text());
});
const { data } = await fetchAxios.get('/', {
responseType: 'formdata',
});
assert.ok(data instanceof FormData, 'data is not instanceof FormData');
assert.deepStrictEqual(
Object.fromEntries(data.entries()),
Object.fromEntries(originalData.entries())
);
}, 5000);
it('should support json response type', async () => {
const originalData = { x: 'my data' };
server = await startHTTPServer((req, res) => res.end(JSON.stringify(originalData)));
const { data } = await fetchAxios.get('/', {
responseType: 'json',
});
assert.deepStrictEqual(data, originalData);
});
});
describe('progress', () => {
describe('upload', () => {
it('should support upload progress capturing', async () => {
server = await startHTTPServer({
rate: 100 * 1024,
});
let content = '';
const count = 10;
const chunk = 'test';
const chunkLength = Buffer.byteLength(chunk);
const contentLength = count * chunkLength;
const readable = stream.Readable.from(
(async function* () {
let i = count;
while (i-- > 0) {
await setTimeoutAsync(1100);
content += chunk;
yield chunk;
}
})()
);
const samples = [];
const { data } = await fetchAxios.post('/', 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,
};
}
})()
)
);
}, 15000);
it('should not fail with get method', async () => {
server = await startHTTPServer((req, res) => res.end('OK'));
const { data } = await fetchAxios.get('/', {
onUploadProgress() {},
});
assert.strictEqual(data, 'OK');
});
});
describe('download', () => {
it('should support download progress capturing', async () => {
server = await startHTTPServer({
rate: 100 * 1024,
});
let content = '';
const count = 10;
const chunk = 'test';
const chunkLength = Buffer.byteLength(chunk);
const contentLength = count * chunkLength;
const readable = stream.Readable.from(
(async function* () {
let i = count;
while (i-- > 0) {
await setTimeoutAsync(1100);
content += chunk;
yield chunk;
}
})()
);
const samples = [];
const { data } = await fetchAxios.post('/', 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,
};
}
})()
)
);
}, 15000);
});
});
it('should support basic auth', async () => {
server = await startHTTPServer((req, res) => res.end(req.headers.authorization));
const user = 'foo';
const headers = { Authorization: 'Bearer 1234' };
const res = await axios.get(`http://${user}@localhost:4444/`, { headers });
const base64 = Buffer.from(`${user}:`, 'utf8').toString('base64');
assert.equal(res.data, `Basic ${base64}`);
});
it('should support stream.Readable as a payload', async () => {
server = await startHTTPServer();
const { data } = await fetchAxios.post('/', stream.Readable.from('OK'));
assert.strictEqual(data, 'OK');
});
describe('request aborting', () => {
it('should be able to abort the request stream', async () => {
server = await startHTTPServer({
rate: 100000,
useBuffering: true,
});
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 500);
await assert.rejects(async () => {
await fetchAxios.post('/', makeReadableStream(), {
responseType: 'stream',
signal: controller.signal,
});
}, /CanceledError/);
});
it('should be able to abort the response stream', async () => {
server = await startHTTPServer((req, res) => {
pipelineAsync(generateReadable(10000, 10), res).catch(() => {
// Client-side abort intentionally closes the stream early in this test.
});
});
const controller = new AbortController();
setTimeout(() => {
controller.abort(new Error('test'));
}, 800);
const { data } = await fetchAxios.get('/', {
responseType: 'stream',
signal: controller.signal,
});
await assert.rejects(async () => {
await data.pipeTo(makeEchoStream());
}, /^(AbortError|CanceledError):/);
});
});
it('should support a timeout', async () => {
server = await startHTTPServer(async (req, res) => {
await setTimeoutAsync(1000);
res.end('OK');
});
const timeout = 500;
const ts = Date.now();
await assert.rejects(async () => {
await fetchAxios('/', {
timeout,
});
}, /timeout/);
const passed = Date.now() - ts;
assert.ok(passed >= timeout - 5, `early cancellation detected (${passed} ms)`);
});
it('should combine baseURL and url', async () => {
server = await startHTTPServer();
const res = await fetchAxios('/foo');
assert.equal(res.config.baseURL, LOCAL_SERVER_URL);
assert.equal(res.config.url, '/foo');
});
it('should support params', async () => {
server = await startHTTPServer((req, res) => res.end(req.url));
const { data } = await fetchAxios.get('/?test=1', {
params: {
foo: 1,
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 () => {
try {
await fetchAxios('http://notExistsUrl.in.nowhere');
assert.fail('should fail');
} catch (err) {
assert.strictEqual(String(err), 'AxiosError: Network Error');
assert.strictEqual(err.cause && err.cause.code, 'ENOTFOUND');
}
});
it('should get response headers', async () => {
server = await startHTTPServer((req, res) => {
res.setHeader('foo', 'bar');
res.end(req.url);
});
const { headers } = await fetchAxios.get('/', {
responseType: 'stream',
});
assert.strictEqual(headers.get('foo'), 'bar');
});
describe('fetch adapter - Content-Type handling', () => {
it('should set correct Content-Type for FormData automatically', async () => {
const form = new NodeFormData();
form.append('foo', 'bar');
server = await startHTTPServer((req, res) => {
const contentType = req.headers['content-type'];
assert.match(contentType, /^multipart\/form-data; boundary=/i);
res.end('OK');
});
await fetchAxios.post('/form', form);
});
});
describe('env config', () => {
it('should respect env fetch API configuration', async () => {
const { data, headers } = await fetchAxios.get('/', {
env: {
fetch() {
return {
headers: {
foo: '1',
},
text: async () => 'test',
};
},
},
});
assert.strictEqual(headers.get('foo'), '1');
assert.strictEqual(data, 'test');
});
it('should be able to request with lack of Request object', async () => {
const form = new FormData();
form.append('x', '1');
const { data, headers } = await fetchAxios.post('/', form, {
onUploadProgress() {
// dummy listener to activate streaming
},
env: {
Request: null,
fetch() {
return {
headers: {
foo: '1',
},
text: async () => 'test',
};
},
},
});
assert.strictEqual(headers.get('foo'), '1');
assert.strictEqual(data, 'test');
});
it('should be able to handle response with lack of Response object', async () => {
const { data, headers } = await fetchAxios.get('/', {
onDownloadProgress() {
// dummy listener to activate streaming
},
env: {
Request: null,
Response: null,
fetch() {
return {
headers: {
foo: '1',
},
text: async () => 'test',
};
},
},
});
assert.strictEqual(headers.get('foo'), '1');
assert.strictEqual(data, 'test');
});
it('should fallback to the global on undefined env value', async () => {
server = await startHTTPServer((req, res) => res.end('OK'));
const { data } = await fetchAxios.get('/', {
env: {
fetch: undefined,
},
});
assert.strictEqual(data, 'OK');
});
it('should use current global fetch when env fetch is not specified', async () => {
const globalFetch = global.fetch;
vi.stubGlobal('fetch', async () => {
return {
headers: {
foo: '1',
},
text: async () => 'global',
};
});
try {
server = await startHTTPServer((req, res) => res.end('OK'));
const { data } = await fetchAxios.get('/', {
env: {
fetch: undefined,
},
});
assert.strictEqual(data, 'global');
} finally {
vi.stubGlobal('fetch', globalFetch);
}
});
});
});
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA+joLfff3WdFIGwrlVdUWfZ9vDr9w86yQpI6Xq0KF6JiKtxa+
EVCNWtq8Ht5rZd1MxNSFWVb1jxnRLBEB8F3wLD/4xRk8CNxw8joXKsxUEirI76iV
mJDjm/tq90gaxZUWVtPZP95Qvocdl7brhuvLuF4J9wJVL5tr6ZYAje3omHBnjcFb
n9nh3Tqk0U6omAmS4k/BrXWDA1zq8LBmbP41CnfVNGq/5asDIZsNO8s5VQzNjFjN
rcqv2rEO0n+77qDBbqALQydxPmytZP+0jYXcj+EaJ4clApFhvps6Cg0taoF66E4k
Eg1JLALzgk3I/ATSE3nvi3J/RWkyjSYiolPavwIDAQABAoIBAEbMi5ndwjfAlkVI
hPEPNKjgpnymwB/CEL7utY04akkQeBcrsSWXBBfT0exuBDczMVhzxTMs/pe5t0xf
l4vaGG18wDeMV0cukCqJMyrh21u0jVv5+DHNtQjaTz6eQSzsbQCuOkbu8SuncUEO
+X8YUnDc8rbYCyBIOnVCAvAlg201uW0G5G9NEwJOu6cAKMKkogdHqv+FRX96C5hm
gtbGEzpGV2vVClgMwMcX49ucluZvqLvit/yehNVd0VOtW/kuLup4R6q0abHRapDd
95rJAhPvar4mzP+UgJrGQ9hozqhizDthBjnsmGeMBUiBCkay7OXIZpvLoCpQkti1
WIWuikkCgYEA/oZqq71RT1nPuI7rlcjx3AeWe2EUQtKhQMJBiPx5eLLP6gII8+v2
pD1qlmJM2eyIK0lzuskLIulTAA5Z+ejORDbvmn/DdT0CSvdrUFrcvnrRQnt2M5M2
9VDRp6nvPE0H4kRZJrtITyLn0dv5ABf2L32i4dPCMePjKjSUygJSHrsCgYEA+61A
cIqch/lrQTk8hG7Y6p0EJzSInFVaKuZoMYpLhlDQcVvSDIQbGgRAN6BKTdxeQ+tK
hSxBSm2mze11aHig8GBGgdBFLaJOZRo6G+2fl+s1t1FCHfsaFhHwheZJONHMpKKd
Qm/7L/V35QV9YG0lPZ01TM6d5lXuKsmUNvBJTc0CgYASYajAgGqn3WeX/5JZ/eoh
ptaiUG+DJ+0HXUAYYYtwQRGs57q3yvnEAL963tyH/IIVBjf6bFyGh+07ms26s6p5
2LHTKZj3FZHd0iKI6hb5FquYLoxpyx7z9oM9pZMmerWwDJmXp3zgYjf1uvovnItm
AJ/LyVxD+B5GxQdd028U0wKBgG4OllZglxDzJk7wa6FyI9N89Fr8oxzSSkrmVPwN
APfskSpxP8qPXpai8z4gDz47NtG2q/DOqIKWrtHwnF4iGibjwxFzdTz+dA/MR0r9
P8QcbHIMy7/2lbK/B5JWYQDC5h28qs5pz8tqKZLyMqCfOiDWhX9f/zbBrxPw8KqR
q0ylAoGAL/0kemA/Tmxpwmp0S0oCqnA4gbCgS7qnApxB09xTewc/tuvraXc3Mzea
EvqDXLXK0R7O4E3vo0Mr23SodRVlFPevsmUUJLPJMJcxdfnSJgX+qE/UC8Ux+UMi
eYufYRDYSslfL2rt9D7abnnbqSfsHymJKukWpElIgJTklQUru4k=
-----END RSA PRIVATE KEY-----
+55
View File
@@ -0,0 +1,55 @@
import { describe, it } from 'vitest';
import assert from 'assert';
import Axios from '../../lib/core/Axios.js';
describe('Axios', () => {
describe('handle un-writable error stack', () => {
const testUnwritableErrorStack = async (stackAttributes) => {
const axios = new Axios({});
// Mock axios._request to return an Error with an un-writable stack property.
axios._request = () => {
const mockError = new Error('test-error');
Object.defineProperty(mockError, 'stack', stackAttributes);
throw mockError;
};
try {
await axios.request('test-url', {});
} catch (e) {
assert.strictEqual(e.message, 'test-error');
}
};
it('should support errors with a defined but un-writable stack', async () => {
await testUnwritableErrorStack({ value: {}, writable: false });
});
it('should support errors with an undefined and un-writable stack', async () => {
await testUnwritableErrorStack({ value: undefined, writable: false });
});
it('should support errors with a custom getter/setter for the stack property', async () => {
await testUnwritableErrorStack({
get: () => ({}),
set: () => {
throw new Error('read-only');
},
});
});
it('should support errors with a custom getter/setter for the stack property (null case)', async () => {
await testUnwritableErrorStack({
get: () => null,
set: () => {
throw new Error('read-only');
},
});
});
});
it('should not throw if the config argument is omitted', () => {
const axios = new Axios();
assert.deepStrictEqual(axios.defaults, {});
});
});
+497
View File
@@ -0,0 +1,497 @@
import { describe, it } from 'vitest';
import assert from 'assert';
import AxiosHeaders from '../../lib/core/AxiosHeaders.js';
const [nodeMajorVersion] = process.versions.node.split('.').map((v) => parseInt(v, 10));
describe('AxiosHeaders', () => {
it('should support headers argument', () => {
const headers = new AxiosHeaders({
x: 1,
y: 2,
});
assert.strictEqual(headers.get('x'), '1');
assert.strictEqual(headers.get('y'), '2');
});
describe('set', () => {
it('should support adding a single header', () => {
const headers = new AxiosHeaders();
headers.set('foo', 'bar');
assert.strictEqual(headers.get('foo'), 'bar');
});
it('should support adding multiple headers', () => {
const headers = new AxiosHeaders();
headers.set({
foo: 'value1',
bar: 'value2',
});
assert.strictEqual(headers.get('foo'), 'value1');
assert.strictEqual(headers.get('bar'), 'value2');
});
it('should support adding multiple headers from raw headers string', () => {
const headers = new AxiosHeaders();
headers.set(`foo:value1\nbar:value2`);
assert.strictEqual(headers.get('foo'), 'value1');
assert.strictEqual(headers.get('bar'), 'value2');
});
it('should not rewrite header the header if the value is false', () => {
const headers = new AxiosHeaders();
headers.set('foo', 'value1');
headers.set('foo', 'value2', false);
assert.strictEqual(headers.get('foo'), 'value1');
headers.set('foo', 'value2');
assert.strictEqual(headers.get('foo'), 'value2');
headers.set('foo', 'value3', true);
assert.strictEqual(headers.get('foo'), 'value3');
});
it('should not rewrite the header if its value is false, unless rewrite options is set to true', () => {
const headers = new AxiosHeaders();
headers.set('foo', false);
headers.set('foo', 'value2');
assert.strictEqual(headers.get('foo'), false);
headers.set('foo', 'value2', true);
assert.strictEqual(headers.get('foo'), 'value2');
});
it('should support iterables as a key-value source object', () => {
const headers = new AxiosHeaders();
headers.set(new Map([['x', '123']]));
assert.strictEqual(headers.get('x'), '123');
});
const runIfNode18OrHigher = nodeMajorVersion >= 18 ? it : it.skip;
runIfNode18OrHigher('should support setting multiple header values from an iterable source', () => {
const headers = new AxiosHeaders();
const nativeHeaders = new Headers();
nativeHeaders.append('set-cookie', 'foo');
nativeHeaders.append('set-cookie', 'bar');
nativeHeaders.append('set-cookie', 'baz');
nativeHeaders.append('y', 'qux');
headers.set(nativeHeaders);
assert.deepStrictEqual(headers.get('set-cookie'), ['foo', 'bar', 'baz']);
assert.strictEqual(headers.get('y'), 'qux');
});
});
it('should support uppercase name mapping for names overlapped by class methods', () => {
const headers = new AxiosHeaders({
set: 'foo',
});
headers.set('get', 'bar');
assert.strictEqual(headers.get('Set'), 'foo');
assert.strictEqual(headers.get('Get'), 'bar');
});
describe('get', () => {
describe('filter', () => {
it('should support RegExp', () => {
const headers = new AxiosHeaders();
headers.set('foo', 'bar=value1');
assert.strictEqual(headers.get('foo', /^bar=(\w+)/)[1], 'value1');
assert.strictEqual(headers.get('foo', /^foo=/), null);
});
it('should support function', () => {
const headers = new AxiosHeaders();
headers.set('foo', 'bar=value1');
assert.strictEqual(
headers.get('foo', (value, header) => {
assert.strictEqual(value, 'bar=value1');
assert.strictEqual(header, 'foo');
return value;
}),
'bar=value1'
);
assert.strictEqual(
headers.get('foo', () => false),
false
);
});
});
});
describe('has', () => {
it('should return true if the header is defined, otherwise false', () => {
const headers = new AxiosHeaders();
headers.set('foo', 'bar=value1');
assert.strictEqual(headers.has('foo'), true);
assert.strictEqual(headers.has('bar'), false);
});
describe('filter', () => {
it('should support RegExp', () => {
const headers = new AxiosHeaders();
headers.set('foo', 'bar=value1');
assert.strictEqual(headers.has('foo', /^bar=(\w+)/), true);
assert.strictEqual(headers.has('foo', /^foo=/), false);
});
it('should support function', () => {
const headers = new AxiosHeaders();
headers.set('foo', 'bar=value1');
assert.strictEqual(
headers.has('foo', (value, header) => {
assert.strictEqual(value, 'bar=value1');
assert.strictEqual(header, 'foo');
return true;
}),
true
);
assert.strictEqual(
headers.has('foo', () => false),
false
);
});
it('should support string pattern', () => {
const headers = new AxiosHeaders();
headers.set('foo', 'bar=value1');
assert.strictEqual(headers.has('foo', 'value1'), true);
assert.strictEqual(headers.has('foo', 'value2'), false);
});
});
});
describe('delete', () => {
it('should delete the header', () => {
const headers = new AxiosHeaders();
headers.set('foo', 'bar=value1');
assert.strictEqual(headers.has('foo'), true);
headers.delete('foo');
assert.strictEqual(headers.has('foo'), false);
});
it('should return true if the header has been deleted, otherwise false', () => {
const headers = new AxiosHeaders();
headers.set('foo', 'bar=value1');
assert.strictEqual(headers.delete('bar'), false);
assert.strictEqual(headers.delete('foo'), true);
});
it('should support headers array', () => {
const headers = new AxiosHeaders();
headers.set('foo', 'x');
headers.set('bar', 'y');
headers.set('baz', 'z');
assert.strictEqual(headers.delete(['foo', 'baz']), true);
assert.strictEqual(headers.has('foo'), false);
assert.strictEqual(headers.has('bar'), true);
assert.strictEqual(headers.has('baa'), false);
});
describe('filter', () => {
it('should support RegExp', () => {
const headers = new AxiosHeaders();
headers.set('foo', 'bar=value1');
assert.strictEqual(headers.has('foo'), true);
headers.delete('foo', /baz=/);
assert.strictEqual(headers.has('foo'), true);
headers.delete('foo', /bar=/);
assert.strictEqual(headers.has('foo'), false);
});
it('should support function', () => {
const headers = new AxiosHeaders();
headers.set('foo', 'bar=value1');
headers.delete('foo', (value, header) => {
assert.strictEqual(value, 'bar=value1');
assert.strictEqual(header, 'foo');
return false;
});
assert.strictEqual(headers.has('foo'), true);
assert.strictEqual(
headers.delete('foo', () => true),
true
);
assert.strictEqual(headers.has('foo'), false);
});
it('should support string pattern', () => {
const headers = new AxiosHeaders();
headers.set('foo', 'bar=value1');
assert.strictEqual(headers.has('foo'), true);
headers.delete('foo', 'baz');
assert.strictEqual(headers.has('foo'), true);
headers.delete('foo', 'bar');
assert.strictEqual(headers.has('foo'), false);
});
});
});
describe('clear', () => {
it('should clear all headers', () => {
const headers = new AxiosHeaders({ x: 1, y: 2 });
headers.clear();
assert.deepStrictEqual({ ...headers.toJSON() }, {});
});
it('should clear matching headers if a matcher was specified', () => {
const headers = new AxiosHeaders({ foo: 1, 'x-foo': 2, bar: 3 });
assert.deepStrictEqual({ ...headers.toJSON() }, { foo: '1', 'x-foo': '2', bar: '3' });
headers.clear(/^x-/);
assert.deepStrictEqual({ ...headers.toJSON() }, { foo: '1', bar: '3' });
});
});
describe('toJSON', () => {
it('should return headers object with original headers case', () => {
const headers = new AxiosHeaders({
Foo: 'x',
bAr: 'y',
});
assert.deepStrictEqual(
{ ...headers.toJSON() },
{
Foo: 'x',
bAr: 'y',
}
);
});
});
describe('accessors', () => {
it('should support get accessor', () => {
const headers = new AxiosHeaders({
foo: 1,
});
headers.constructor.accessor('foo');
assert.strictEqual(typeof headers.getFoo, 'function');
assert.strictEqual(headers.getFoo(), '1');
});
it('should support set accessor', () => {
const headers = new AxiosHeaders({
foo: 1,
});
headers.constructor.accessor('foo');
assert.strictEqual(typeof headers.setFoo, 'function');
headers.setFoo(2);
assert.strictEqual(headers.getFoo(), '2');
});
it('should support has accessor', () => {
const headers = new AxiosHeaders({
foo: 1,
});
headers.constructor.accessor('foo');
assert.strictEqual(typeof headers.hasFoo, 'function');
assert.strictEqual(headers.hasFoo(), true);
});
});
it('should be caseless', () => {
const headers = new AxiosHeaders({
fOo: 1,
});
assert.strictEqual(headers.get('Foo'), '1');
assert.strictEqual(headers.get('foo'), '1');
headers.set('foo', 2);
assert.strictEqual(headers.get('foO'), '2');
assert.strictEqual(headers.get('fOo'), '2');
assert.strictEqual(headers.has('fOo'), true);
headers.delete('FOO');
assert.strictEqual(headers.has('fOo'), false);
});
describe('normalize()', () => {
it('should support auto-formatting', () => {
const headers = new AxiosHeaders({
fOo: 1,
'x-foo': 2,
'y-bar-bAz': 3,
});
assert.deepStrictEqual(
{ ...headers.normalize(true).toJSON() },
{
Foo: '1',
'X-Foo': '2',
'Y-Bar-Baz': '3',
}
);
});
it('should support external defined values', () => {
const headers = new AxiosHeaders({
foo: '1',
});
headers.Foo = 2;
headers.bar = 3;
assert.deepStrictEqual(
{ ...headers.normalize().toJSON() },
{
foo: '2',
bar: '3',
}
);
});
it('should support array values', () => {
const headers = new AxiosHeaders({
foo: [1, 2, 3],
});
assert.deepStrictEqual(
{ ...headers.normalize().toJSON() },
{
foo: ['1', '2', '3'],
}
);
});
});
describe('AxiosHeaders.concat', () => {
it('should concatenate plain headers into an AxiosHeader instance', () => {
const a = { a: 1 };
const b = { b: 2 };
const c = { c: 3 };
const headers = AxiosHeaders.concat(a, b, c);
assert.deepStrictEqual(
{ ...headers.toJSON() },
{
a: '1',
b: '2',
c: '3',
}
);
});
it('should concatenate raw headers into an AxiosHeader instance', () => {
const a = 'a:1\nb:2';
const b = 'c:3\nx:4';
const headers = AxiosHeaders.concat(a, b);
assert.deepStrictEqual(
{ ...headers.toJSON() },
{
a: '1',
b: '2',
c: '3',
x: '4',
}
);
});
it('should concatenate Axios headers into a new AxiosHeader instance', () => {
const a = new AxiosHeaders({ x: 1 });
const b = new AxiosHeaders({ y: 2 });
const headers = AxiosHeaders.concat(a, b);
assert.deepStrictEqual(
{ ...headers.toJSON() },
{
x: '1',
y: '2',
}
);
});
});
describe('toString', () => {
it('should serialize AxiosHeader instance to a raw headers string', () => {
assert.deepStrictEqual(new AxiosHeaders({ x: 1, y: 2 }).toString(), 'x: 1\ny: 2');
});
});
describe('getSetCookie', () => {
it('should return set-cookie', () => {
const headers = new AxiosHeaders('Set-Cookie: key=val;\n' + 'Set-Cookie: key2=val2;\n');
assert.deepStrictEqual(headers.getSetCookie(), ['key=val;', 'key2=val2;']);
});
it('should return empty set-cookie', () => {
assert.deepStrictEqual(new AxiosHeaders().getSetCookie(), []);
});
});
});
+40
View File
@@ -0,0 +1,40 @@
import { describe, it } from 'vitest';
import assert from 'assert';
import composeSignals from '../../lib/helpers/composeSignals.js';
describe('helpers::composeSignals', () => {
const runIfAbortController = typeof AbortController === 'function' ? it : it.skip;
runIfAbortController('should abort when any of the signals abort', () => {
let called;
const controllerA = new AbortController();
const controllerB = new AbortController();
const signal = composeSignals([controllerA.signal, controllerB.signal]);
signal.addEventListener('abort', () => {
called = true;
});
controllerA.abort(new Error('test'));
assert.ok(called);
});
runIfAbortController('should abort on timeout', async () => {
const signal = composeSignals([], 100);
await new Promise((resolve) => {
signal.addEventListener('abort', resolve);
});
assert.match(String(signal.reason), /timeout of 100ms exceeded/);
});
it('should return undefined if signals and timeout are not provided', () => {
const signal = composeSignals([]);
assert.strictEqual(signal, undefined);
});
});
@@ -0,0 +1,31 @@
import { describe, it } from 'vitest';
import assert from 'assert';
import estimateDataURLDecodedBytes from '../../lib/helpers/estimateDataURLDecodedBytes.js';
describe('estimateDataURLDecodedBytes', () => {
it('should return 0 for non-data URLs', () => {
assert.strictEqual(estimateDataURLDecodedBytes('http://example.com'), 0);
});
it('should calculate length for simple non-base64 data URL', () => {
const url = 'data:,Hello';
assert.strictEqual(estimateDataURLDecodedBytes(url), Buffer.byteLength('Hello', 'utf8'));
});
it('should calculate decoded length for base64 data URL', () => {
const str = 'Hello';
const b64 = Buffer.from(str, 'utf8').toString('base64');
const url = `data:text/plain;base64,${b64}`;
assert.strictEqual(estimateDataURLDecodedBytes(url), str.length);
});
it('should handle base64 with = padding', () => {
const url = 'data:text/plain;base64,TQ==';
assert.strictEqual(estimateDataURLDecodedBytes(url), 1);
});
it('should handle base64 with %3D padding', () => {
const url = 'data:text/plain;base64,TQ%3D%3D';
assert.strictEqual(estimateDataURLDecodedBytes(url), 1);
});
});
+13
View File
@@ -0,0 +1,13 @@
import { describe, it } from 'vitest';
import assert from 'assert';
import fromDataURI from '../../lib/helpers/fromDataURI.js';
describe('helpers::fromDataURI', () => {
it('should return buffer from data uri', () => {
const buffer = Buffer.from('123');
const dataURI = 'data:application/octet-stream;base64,' + buffer.toString('base64');
assert.deepStrictEqual(fromDataURI(dataURI, false), buffer);
});
});
+28
View File
@@ -0,0 +1,28 @@
import { describe, it } from 'vitest';
import assert from 'assert';
import utils from '../../lib/utils.js';
import parseProtocol from '../../lib/helpers/parseProtocol.js';
describe('helpers::parseProtocol', () => {
it('should parse protocol part if it exists', () => {
utils.forEach(
{
'http://username:password@example.com/': 'http',
'ftp:google.com': 'ftp',
'sms:+15105550101?body=hello%20there': 'sms',
'tel:0123456789': 'tel',
'//google.com': '',
'google.com': '',
'admin://etc/default/grub': 'admin',
'stratum+tcp://server:port': 'stratum+tcp',
'/api/resource:customVerb': '',
'https://stackoverflow.com/questions/': 'https',
'mailto:jsmith@example.com': 'mailto',
'chrome-extension://1234/<pageName>.html': 'chrome-extension',
},
(expectedProtocol, url) => {
assert.strictEqual(parseProtocol(url), expectedProtocol);
}
);
});
});
+23
View File
@@ -0,0 +1,23 @@
import { describe, it } from 'vitest';
import platform from '../../lib/platform/index.js';
import assert from 'assert';
describe('generateString', () => {
it('should generate a string of the specified length using the default alphabet', () => {
const size = 10;
const str = platform.generateString(size);
assert.strictEqual(str.length, size);
});
it('should generate a string using only characters from the default alphabet', () => {
const size = 10;
const alphabet = platform.ALPHABET.ALPHA_DIGIT;
const str = platform.generateString(size, alphabet);
for (let char of str) {
assert.ok(alphabet.includes(char), `Character ${char} is not in the alphabet`);
}
});
});
+211
View File
@@ -0,0 +1,211 @@
/* eslint-disable no-prototype-builtins */
import { afterEach, describe, it } from 'vitest';
import assert from 'assert';
import utils from '../../lib/utils.js';
import mergeConfig from '../../lib/core/mergeConfig.js';
describe('Prototype Pollution Protection', () => {
afterEach(() => {
// Clean up any pollution that might have occurred.
delete Object.prototype.polluted;
});
describe('utils.merge', () => {
it('should filter __proto__ key at top level', () => {
const result = utils.merge({}, { __proto__: { polluted: 'yes' }, safe: 'value' });
assert.strictEqual(Object.prototype.polluted, undefined);
assert.strictEqual(result.safe, 'value');
assert.strictEqual(result.hasOwnProperty('__proto__'), false);
});
it('should filter constructor key at top level', () => {
const result = utils.merge({}, { constructor: { polluted: 'yes' }, safe: 'value' });
assert.strictEqual(result.safe, 'value');
assert.strictEqual(result.hasOwnProperty('constructor'), false);
});
it('should filter prototype key at top level', () => {
const result = utils.merge({}, { prototype: { polluted: 'yes' }, safe: 'value' });
assert.strictEqual(result.safe, 'value');
assert.strictEqual(result.hasOwnProperty('prototype'), false);
});
it('should filter __proto__ key in nested objects', () => {
const result = utils.merge(
{},
{
headers: {
__proto__: { polluted: 'nested' },
'Content-Type': 'application/json',
},
}
);
assert.strictEqual(Object.prototype.polluted, undefined);
assert.strictEqual(result.headers['Content-Type'], 'application/json');
assert.strictEqual(result.headers.hasOwnProperty('__proto__'), false);
});
it('should filter constructor key in nested objects', () => {
const result = utils.merge(
{},
{
headers: {
constructor: { prototype: { polluted: 'nested' } },
'Content-Type': 'application/json',
},
}
);
assert.strictEqual(Object.prototype.polluted, undefined);
assert.strictEqual(result.headers['Content-Type'], 'application/json');
assert.strictEqual(result.headers.hasOwnProperty('constructor'), false);
});
it('should filter prototype key in nested objects', () => {
const result = utils.merge(
{},
{
headers: {
prototype: { polluted: 'nested' },
'Content-Type': 'application/json',
},
}
);
assert.strictEqual(result.headers['Content-Type'], 'application/json');
assert.strictEqual(result.headers.hasOwnProperty('prototype'), false);
});
it('should filter dangerous keys in deeply nested objects', () => {
const result = utils.merge(
{},
{
level1: {
level2: {
__proto__: { polluted: 'deep' },
prototype: { polluted: 'deep' },
safe: 'value',
},
},
}
);
assert.strictEqual(Object.prototype.polluted, undefined);
assert.strictEqual(result.level1.level2.safe, 'value');
assert.strictEqual(result.level1.level2.hasOwnProperty('__proto__'), false);
});
it('should still merge regular properties correctly', () => {
const result = utils.merge({ a: 1, b: { c: 2 } }, { b: { d: 3 }, e: 4 });
assert.strictEqual(result.a, 1);
assert.strictEqual(result.b.c, 2);
assert.strictEqual(result.b.d, 3);
assert.strictEqual(result.e, 4);
});
it('should handle JSON.parse payloads safely', () => {
const malicious = JSON.parse('{"__proto__": {"polluted": "yes"}}');
const result = utils.merge({}, malicious);
assert.strictEqual(Object.prototype.polluted, undefined);
assert.strictEqual(result.hasOwnProperty('__proto__'), false);
});
it('should handle nested JSON.parse payloads safely', () => {
const malicious = JSON.parse(
'{"headers": {"constructor": {"prototype": {"polluted": "yes"}}}}'
);
const result = utils.merge({}, malicious);
assert.strictEqual(Object.prototype.polluted, undefined);
assert.strictEqual(result.headers.hasOwnProperty('constructor'), false);
});
});
describe('mergeConfig', () => {
it('should filter dangerous keys at top level', () => {
const result = mergeConfig(
{},
{
__proto__: { polluted: 'yes' },
constructor: { polluted: 'yes' },
prototype: { polluted: 'yes' },
url: '/api/test',
}
);
assert.strictEqual(Object.prototype.polluted, undefined);
assert.strictEqual(result.url, '/api/test');
assert.strictEqual(result.hasOwnProperty('__proto__'), false);
assert.strictEqual(result.hasOwnProperty('constructor'), false);
assert.strictEqual(result.hasOwnProperty('prototype'), false);
});
it('should filter dangerous keys in headers', () => {
const result = mergeConfig(
{},
{
headers: {
__proto__: { polluted: 'yes' },
'Content-Type': 'application/json',
},
}
);
assert.strictEqual(Object.prototype.polluted, undefined);
assert.strictEqual(result.headers['Content-Type'], 'application/json');
assert.strictEqual(result.headers.hasOwnProperty('__proto__'), false);
});
it('should filter dangerous keys in custom config properties', () => {
const result = mergeConfig(
{},
{
customProp: {
__proto__: { polluted: 'yes' },
safe: 'value',
},
}
);
assert.strictEqual(Object.prototype.polluted, undefined);
assert.strictEqual(result.customProp.safe, 'value');
assert.strictEqual(result.customProp.hasOwnProperty('__proto__'), false);
});
it('should still merge configs correctly', () => {
const config1 = {
baseURL: 'https://api.example.com',
timeout: 1000,
headers: {
common: {
Accept: 'application/json',
},
},
};
const config2 = {
url: '/users',
timeout: 5000,
headers: {
common: {
'Content-Type': 'application/json',
},
},
};
const result = mergeConfig(config1, config2);
assert.strictEqual(result.baseURL, 'https://api.example.com');
assert.strictEqual(result.url, '/users');
assert.strictEqual(result.timeout, 5000);
assert.strictEqual(result.headers.common.Accept, 'application/json');
assert.strictEqual(result.headers.common['Content-Type'], 'application/json');
});
});
});
+263
View File
@@ -0,0 +1,263 @@
/**
* Combined regression tests (issues 4999, 5028, 7364 + SSRF SNYK-1038255, SNYK-7361793).
*/
import { describe, it, beforeEach, afterEach, vi } from 'vitest';
import assert from 'assert';
import http from 'http';
import axios from '../../index.js';
import platform from '../../lib/platform/index.js';
describe('regression', () => {
describe('issues', () => {
describe('4999', () => {
// Depends on network: https://postman-echo.com
it('should not fail with query parsing', async () => {
const { data } = await axios.get('https://postman-echo.com/get?foo1=bar1&foo2=bar2');
assert.strictEqual(data.args.foo1, 'bar1');
assert.strictEqual(data.args.foo2, 'bar2');
});
});
describe('5028', () => {
it('should handle set-cookie headers as an array', async () => {
const cookie1 =
'something=else; path=/; expires=Wed, 12 Apr 2023 12:03:42 GMT; samesite=lax; secure; httponly';
const cookie2 =
'something-ssr.sig=n4MlwVAaxQAxhbdJO5XbUpDw-lA; path=/; expires=Wed, 12 Apr 2023 12:03:42 GMT; samesite=lax; secure; httponly';
const server = http
.createServer((req, res) => {
res.setHeader('Set-Cookie', [cookie1, cookie2]);
res.writeHead(200);
res.write('Hi there');
res.end();
})
.listen(0);
const request = axios.create();
request.interceptors.response.use((res) => {
assert.deepStrictEqual(res.headers['set-cookie'], [cookie1, cookie2]);
});
try {
await request({ url: `http://localhost:${server.address().port}` });
} finally {
server.close();
}
});
});
describe('7364', () => {
it('fetch: should have status code in axios error', async () => {
const isFetchSupported = typeof fetch === 'function';
if (!isFetchSupported) {
vi.skip();
}
const server = http
.createServer((req, res) => {
res.statusCode = 400;
res.end();
})
.listen(0);
const instance = axios.create({
baseURL: `http://localhost:${server.address().port}`,
adapter: 'fetch',
});
try {
await instance.get('/status/400');
} catch (error) {
assert.equal(error.name, 'AxiosError');
assert.equal(error.isAxiosError, true);
assert.equal(error.status, 400);
} finally {
server.close();
}
});
it('http: should have status code in axios error', async () => {
const server = http
.createServer((req, res) => {
res.statusCode = 400;
res.end();
})
.listen(0);
const instance = axios.create({
baseURL: `http://localhost:${server.address().port}`,
adapter: 'http',
});
try {
await instance.get('/status/400');
} catch (error) {
assert.equal(error.name, 'AxiosError');
assert.equal(error.isAxiosError, true);
assert.equal(error.status, 400);
} finally {
server.close();
}
});
});
});
// https://snyk.io/vuln/SNYK-JS-AXIOS-1038255
// https://github.com/axios/axios/issues/3407
// https://github.com/axios/axios/issues/3369
describe('SSRF SNYK-JS-AXIOS-1038255', () => {
let fail = false;
let proxy;
let server;
let location;
let evilPort;
let proxyPort;
beforeEach(() => {
fail = false;
server = http
.createServer((req, res) => {
fail = true;
res.end('rm -rf /');
})
.listen(0);
evilPort = server.address().port;
proxy = http
.createServer((req, res) => {
if (
new URL(req.url, 'http://' + req.headers.host).toString() ===
'http://localhost:' + evilPort + '/'
) {
return res.end(
JSON.stringify({
msg: 'Protected',
headers: req.headers,
})
);
}
res.writeHead(302, { location });
res.end();
})
.listen(0);
proxyPort = proxy.address().port;
location = 'http://localhost:' + evilPort;
});
afterEach(() => {
server.close();
proxy.close();
});
it('obeys proxy settings when following redirects', async () => {
const response = await axios({
method: 'get',
url: 'http://www.google.com/',
proxy: {
host: 'localhost',
port: proxyPort,
auth: {
username: 'sam',
password: 'password',
},
},
});
assert.strictEqual(fail, false);
assert.strictEqual(response.data.msg, 'Protected');
assert.strictEqual(response.data.headers.host, 'localhost:' + evilPort);
assert.strictEqual(
response.data.headers['proxy-authorization'],
'Basic ' + Buffer.from('sam:password').toString('base64')
);
return response;
});
});
// https://security.snyk.io/vuln/SNYK-JS-AXIOS-7361793
// https://github.com/axios/axios/issues/6463
describe('SSRF SNYK-JS-AXIOS-7361793', () => {
let goodServer;
let badServer;
let goodPort;
let badPort;
beforeEach(() => {
goodServer = http
.createServer((req, res) => {
res.write('good');
res.end();
})
.listen(0);
goodPort = goodServer.address().port;
badServer = http
.createServer((req, res) => {
res.write('bad');
res.end();
})
.listen(0);
badPort = badServer.address().port;
});
afterEach(() => {
goodServer.close();
badServer.close();
});
it('should not fetch in server-side mode', async () => {
const ssrfAxios = axios.create({
baseURL: 'http://localhost:' + String(goodPort),
});
const userId = '/localhost:' + String(badPort);
try {
await ssrfAxios.get(`/${userId}`);
} catch (error) {
assert.ok(error.message.startsWith('Invalid URL'));
return;
}
assert.fail('Expected an error to be thrown');
});
describe('client-side mode', () => {
let savedHasBrowserEnv;
let savedOrigin;
beforeEach(() => {
assert.ok(platform.hasBrowserEnv !== undefined);
savedHasBrowserEnv = platform.hasBrowserEnv;
savedOrigin = platform.origin;
platform.hasBrowserEnv = true;
platform.origin = 'http://localhost:' + String(goodPort);
});
afterEach(() => {
platform.hasBrowserEnv = savedHasBrowserEnv;
platform.origin = savedOrigin;
});
it('resolves URL relative to origin and returns bad server body', async () => {
const ssrfAxios = axios.create({
baseURL: 'http://localhost:' + String(goodPort),
});
const userId = '/localhost:' + String(badPort);
const response = await ssrfAxios.get(`/${userId}`);
assert.strictEqual(response.data, 'bad');
assert.strictEqual(response.config.baseURL, 'http://localhost:' + String(goodPort));
assert.strictEqual(response.config.url, '//localhost:' + String(badPort));
assert.strictEqual(
response.request.res.responseUrl,
'http://localhost:' + String(badPort) + '/'
);
});
});
});
});
+132
View File
@@ -0,0 +1,132 @@
import { describe, it } from 'vitest';
import assert from 'assert';
import FormData from 'form-data';
import toFormData from '../../lib/helpers/toFormData.js';
describe('helpers::toFormData', () => {
const createRNFormDataSpy = () => {
const calls = [];
return {
calls,
append: (key, value) => {
calls.push([key, value]);
},
getParts: () => {
return [];
},
};
};
it('should convert a flat object to FormData', () => {
const data = {
foo: 'bar',
baz: 123,
};
const formData = toFormData(data, new FormData());
assert.ok(formData instanceof FormData);
assert.ok(formData._streams.length > 0);
});
it('should convert a nested object to FormData', () => {
const data = {
foo: {
bar: 'baz',
},
};
const formData = toFormData(data, new FormData());
assert.ok(formData instanceof FormData);
});
it('should throw Error on circular reference', () => {
const data = {
foo: 'bar',
};
data.self = data;
try {
toFormData(data, new FormData());
assert.fail('Should have thrown an error');
} catch (err) {
assert.strictEqual(err.message, 'Circular reference detected in self');
}
});
it('should handle arrays', () => {
const data = {
arr: [1, 2, 3],
};
const formData = toFormData(data, new FormData());
assert.ok(formData instanceof FormData);
});
it('should append root-level React Native blob without recursion', () => {
const formData = createRNFormDataSpy();
const blob = {
uri: 'file://test.png',
type: 'image/png',
name: 'test.png',
};
toFormData({ file: blob }, formData);
assert.strictEqual(formData.calls.length, 1);
assert.strictEqual(formData.calls[0][0], 'file');
assert.strictEqual(formData.calls[0][1], blob);
});
it('should append nested React Native blob without recursion', () => {
const formData = createRNFormDataSpy();
const blob = {
uri: 'file://nested.png',
type: 'image/png',
name: 'nested.png',
};
toFormData({ nested: { file: blob } }, formData);
assert.strictEqual(formData.calls.length, 1);
assert.strictEqual(formData.calls[0][0], 'nested[file]');
assert.strictEqual(formData.calls[0][1], blob);
});
it('should append deeply nested React Native blob without recursion', () => {
const formData = createRNFormDataSpy();
const blob = {
uri: 'file://deep.png',
name: 'deep.png',
};
toFormData({ a: { b: { c: blob } } }, formData);
assert.strictEqual(formData.calls.length, 1);
assert.strictEqual(formData.calls[0][0], 'a[b][c]');
assert.strictEqual(formData.calls[0][1], blob);
});
it('should NOT recurse into React Native blob properties', () => {
const formData = createRNFormDataSpy();
const blob = {
uri: 'file://nope.png',
type: 'image/png',
name: 'nope.png',
};
toFormData({ file: blob }, formData);
const keys = formData.calls.map((call) => call[0]);
assert.deepStrictEqual(keys, ['file']);
assert.ok(!keys.some((key) => key.includes('uri')));
assert.ok(!keys.some((key) => key.includes('type')));
assert.ok(!keys.some((key) => key.includes('name')));
});
});
+70
View File
@@ -0,0 +1,70 @@
import { describe, it } from 'vitest';
import defaults from '../../lib/defaults/index.js';
import transformData from '../../lib/core/transformData.js';
import assert from 'assert';
describe('transformResponse', () => {
describe('200 request', () => {
it('parses json', () => {
const data = '{"message": "hello, world"}';
const result = transformData.call(
{
data,
response: {
headers: { 'content-type': 'application/json' },
status: 200,
},
},
defaults.transformResponse
);
assert.strictEqual(result.message, 'hello, world');
});
it('ignores XML', () => {
const data = '<message>hello, world</message>';
const result = transformData.call(
{
data,
response: {
headers: { 'content-type': 'text/xml' },
status: 200,
},
},
defaults.transformResponse
);
assert.strictEqual(result, data);
});
});
describe('204 request', () => {
it('does not parse the empty string', () => {
const data = '';
const result = transformData.call(
{
data,
response: {
headers: { 'content-type': undefined },
status: 204,
},
},
defaults.transformResponse
);
assert.strictEqual(result, '');
});
it('does not parse undefined', () => {
const data = undefined;
const result = transformData.call(
{
data,
response: {
headers: { 'content-type': undefined },
status: 200,
},
},
defaults.transformResponse
);
assert.strictEqual(result, data);
});
});
});
+175
View File
@@ -0,0 +1,175 @@
import { describe, it } from 'vitest';
import assert from 'assert';
import utils from '../../lib/utils.js';
import FormData from 'form-data';
import stream from 'stream';
describe('utils', () => {
it('should validate Stream', () => {
assert.strictEqual(utils.isStream(new stream.Readable()), true);
assert.strictEqual(utils.isStream({ foo: 'bar' }), false);
});
it('should validate Buffer', () => {
assert.strictEqual(utils.isBuffer(Buffer.from('a')), true);
assert.strictEqual(utils.isBuffer(null), false);
assert.strictEqual(utils.isBuffer(undefined), false);
});
describe('utils::isFormData', () => {
it('should detect the FormData instance provided by the `form-data` package', () => {
[1, 'str', {}, new RegExp()].forEach((thing) => {
assert.equal(utils.isFormData(thing), false);
});
assert.equal(utils.isFormData(new FormData()), true);
});
it('should not call toString method on built-in objects instances', () => {
const buf = Buffer.from('123');
buf.toString = () => assert.fail('should not be called');
assert.equal(utils.isFormData(buf), false);
});
it('should not call toString method on built-in objects instances, even if append method exists', () => {
const buf = Buffer.from('123');
buf.append = () => {};
buf.toString = () => assert.fail('should not be called');
assert.equal(utils.isFormData(buf), false);
});
it('should detect custom FormData instances by toStringTag signature and append method presence', () => {
class FormData {
append() {}
get [Symbol.toStringTag]() {
return 'FormData';
}
}
assert.equal(utils.isFormData(new FormData()), true);
});
});
describe('toJSON', () => {
it('should convert to a plain object without circular references', () => {
const obj = { a: [0] };
const source = { x: 1, y: 2, obj };
source.circular1 = source;
obj.a[1] = obj;
assert.deepStrictEqual(utils.toJSONObject(source), {
x: 1,
y: 2,
obj: { a: [0] },
});
});
it('should use objects with defined toJSON method without rebuilding', () => {
const objProp = {};
const obj = {
objProp,
toJSON() {
return { ok: 1 };
},
};
const source = { x: 1, y: 2, obj };
const jsonObject = utils.toJSONObject(source);
assert.strictEqual(jsonObject.obj.objProp, objProp);
assert.strictEqual(
JSON.stringify(jsonObject),
JSON.stringify({ x: 1, y: 2, obj: { ok: 1 } })
);
});
});
describe('Buffer RangeError Fix', () => {
it('should handle large Buffer in isEmptyObject without RangeError', () => {
const largeBuffer = Buffer.alloc(1024 * 1024 * 200);
const result = utils.isEmptyObject(largeBuffer);
assert.strictEqual(result, false);
});
it('should handle large Buffer in forEach without RangeError', () => {
const largeBuffer = Buffer.alloc(1024 * 1024 * 200);
let count = 0;
utils.forEach(largeBuffer, () => count++);
assert.strictEqual(count, 0);
});
it('should handle large Buffer in findKey without RangeError', () => {
const largeBuffer = Buffer.alloc(1024 * 1024 * 200);
const result = utils.findKey(largeBuffer, 'test');
assert.strictEqual(result, null);
});
});
describe('utils::isReactNativeBlob', () => {
it('should return true for objects with uri property', () => {
assert.strictEqual(utils.isReactNativeBlob({ uri: 'file://path/to/file' }), true);
assert.strictEqual(utils.isReactNativeBlob({ uri: 'content://media/image' }), true);
});
it('should return true for React Native blob-like objects with optional name and type', () => {
assert.strictEqual(
utils.isReactNativeBlob({
uri: 'file://path/to/file',
name: 'image.png',
type: 'image/png',
}),
true
);
});
it('should return false for objects without uri property', () => {
assert.strictEqual(utils.isReactNativeBlob({ path: 'file://path' }), false);
assert.strictEqual(utils.isReactNativeBlob({ url: 'http://example.com' }), false);
assert.strictEqual(utils.isReactNativeBlob({}), false);
});
it('should return false for non-objects', () => {
assert.strictEqual(utils.isReactNativeBlob(null), false);
assert.strictEqual(utils.isReactNativeBlob(undefined), false);
assert.strictEqual(utils.isReactNativeBlob('string'), false);
assert.strictEqual(utils.isReactNativeBlob(123), false);
assert.strictEqual(utils.isReactNativeBlob(false), false);
});
it('should return true even if uri is empty string', () => {
assert.strictEqual(utils.isReactNativeBlob({ uri: '' }), true);
});
});
describe('utils::isReactNative', () => {
it('should return true for FormData with getParts method', () => {
const mockReactNativeFormData = {
append: () => {},
getParts: () => {
return [];
},
};
assert.strictEqual(utils.isReactNative(mockReactNativeFormData), true);
});
it('should return false for standard FormData without getParts method', () => {
const standardFormData = new FormData();
assert.strictEqual(utils.isReactNative(standardFormData), false);
});
it('should return false for objects without getParts method', () => {
assert.strictEqual(utils.isReactNative({ append: () => {} }), false);
assert.strictEqual(utils.isReactNative({}), false);
});
});
});
+30
View File
@@ -0,0 +1,30 @@
import { defineConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
export default defineConfig({
test: {
testTimeout: 10000,
projects: [
{
test: {
name: 'unit',
environment: 'node',
include: ['tests/unit/**/*.test.js'],
setupFiles: [],
},
},
{
test: {
name: 'browser',
include: ['tests/browser/**/*.browser.test.js'],
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
setupFiles: ['tests/setup/browser.setup.js'],
},
},
],
},
});