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:
@@ -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
|
||||
|
||||
Generated
+3421
-2826
File diff suppressed because it is too large
Load Diff
+9
-1
@@ -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
@@ -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' } }]],
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
/* global jasmine */
|
||||
import _axios from '../../index.js';
|
||||
|
||||
window.axios = _axios;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
/* global jasmine */
|
||||
describe('adapter', function () {
|
||||
beforeEach(function () {
|
||||
jasmine.Ajax.install();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
describe('static api', function () {
|
||||
it('should have request method helpers', function () {
|
||||
expect(typeof axios.request).toEqual('function');
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
/* global jasmine */
|
||||
import axios from '../../index';
|
||||
|
||||
function validateInvalidCharacterError(error) {
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import CancelToken from '../../../lib/cancel/CancelToken';
|
||||
import CanceledError from '../../../lib/cancel/CanceledError';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import CanceledError from '../../../lib/cancel/CanceledError';
|
||||
|
||||
describe('Cancel', function () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import isCancel from '../../../lib/cancel/isCancel';
|
||||
import CanceledError from '../../../lib/cancel/CanceledError';
|
||||
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import buildFullPath from '../../../lib/core/buildFullPath';
|
||||
|
||||
describe('helpers::buildFullPath', function () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import defaults from '../../../lib/defaults';
|
||||
import mergeConfig from '../../../lib/core/mergeConfig';
|
||||
import { AxiosHeaders } from '../../../index.js';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import settle from '../../../lib/core/settle';
|
||||
|
||||
describe('core::settle', function () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import transformData from '../../../lib/core/transformData';
|
||||
|
||||
describe('core::transformData', function () {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
/* global jasmine */
|
||||
import defaults from '../../lib/defaults';
|
||||
import AxiosHeaders from '../../lib/core/AxiosHeaders';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import { retryNetwork } from '../helpers/retry.js';
|
||||
|
||||
describe('FormData', function () {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
/* global jasmine */
|
||||
const { AxiosHeaders } = axios;
|
||||
|
||||
function testHeaderValue(headers, key, val) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import bind from '../../../lib/helpers/bind';
|
||||
|
||||
describe('bind', function () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import buildURL from '../../../lib/helpers/buildURL';
|
||||
|
||||
describe('helpers::buildURL', function () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import combineURLs from '../../../lib/helpers/combineURLs';
|
||||
|
||||
describe('helpers::combineURLs', function () {
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import isAbsoluteURL from '../../../lib/helpers/isAbsoluteURL';
|
||||
|
||||
describe('helpers::isAbsoluteURL', function () {
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import parseHeaders from '../../../lib/helpers/parseHeaders';
|
||||
|
||||
describe('helpers::parseHeaders', function () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import spread from '../../../lib/helpers/spread';
|
||||
|
||||
describe('helpers::spread', function () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import toFormData from '../../../lib/helpers/toFormData';
|
||||
|
||||
describe('toFormData', function () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
'use strict';
|
||||
|
||||
import validator from '../../../lib/helpers/validator';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
/* global jasmine */
|
||||
describe('instance', function () {
|
||||
beforeEach(function () {
|
||||
jasmine.Ajax.install();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
/* global jasmine */
|
||||
describe('interceptors', function () {
|
||||
beforeEach(function () {
|
||||
jasmine.Ajax.install();
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/* eslint-env mocha */
|
||||
/* global jasmine */
|
||||
|
||||
// import AxiosHeaders from "../../lib/core/AxiosHeaders.js";
|
||||
// import isAbsoluteURL from '../../lib/helpers/isAbsoluteURL.js';
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
/* global jasmine */
|
||||
describe('progress events', function () {
|
||||
beforeEach(function () {
|
||||
jasmine.Ajax.install();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
/* global jasmine */
|
||||
describe('promise', function () {
|
||||
beforeEach(function () {
|
||||
jasmine.Ajax.install();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
/* global jasmine */
|
||||
describe('requests', function () {
|
||||
beforeEach(function () {
|
||||
jasmine.Ajax.install();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
/* global jasmine */
|
||||
import AxiosError from '../../lib/core/AxiosError';
|
||||
|
||||
describe('transform', function () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import utils from '../../../lib/utils';
|
||||
|
||||
const { kindOf } = utils;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import utils from '../../../lib/utils';
|
||||
|
||||
const { extend } = utils;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import utils from '../../../lib/utils';
|
||||
|
||||
const { forEach } = utils;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import utils from '../../../lib/utils';
|
||||
|
||||
describe('utils::isX', function () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import utils from '../../../lib/utils';
|
||||
|
||||
const { kindOf } = utils;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import { kindOfTest } from '../../../lib/utils';
|
||||
|
||||
describe('utils::kindOfTest', function () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import utils from '../../../lib/utils';
|
||||
|
||||
const { merge } = utils;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import utils from '../../../lib/utils';
|
||||
|
||||
const { toArray } = utils;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import utils from '../../../lib/utils';
|
||||
|
||||
const { toFlatObject } = utils;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import utils from '../../../lib/utils';
|
||||
|
||||
describe('utils::trim', function () {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
/* global jasmine */
|
||||
import cookies from '../../lib/helpers/cookies';
|
||||
|
||||
describe('xsrf', function () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import adapters from '../../../lib/adapters/adapters.js';
|
||||
import assert from 'assert';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import assert from 'assert';
|
||||
import {
|
||||
startHTTPServer,
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import Axios from '../../../lib/core/Axios.js';
|
||||
import assert from 'assert';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import AxiosHeaders from '../../../lib/core/AxiosHeaders.js';
|
||||
import assert from 'assert';
|
||||
|
||||
|
||||
Vendored
+2
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable no-prototype-builtins */
|
||||
/* eslint-env mocha */
|
||||
'use strict';
|
||||
|
||||
import assert from 'assert';
|
||||
|
||||
@@ -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,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,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import assert from 'assert';
|
||||
import fromDataURI from '../../../lib/helpers/fromDataURI.js';
|
||||
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import assert from 'assert';
|
||||
import toFormData from '../../../lib/helpers/toFormData.js';
|
||||
import FormData from 'form-data';
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import assert from 'assert';
|
||||
import http from 'http';
|
||||
import axios from '../../../index.js';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env mocha */
|
||||
import assert from 'assert';
|
||||
import utils from '../../../lib/utils.js';
|
||||
import FormData from 'form-data';
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { afterEach } from 'vitest';
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
@@ -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 }
|
||||
);
|
||||
};
|
||||
@@ -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 |
@@ -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-----
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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-----
|
||||
@@ -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, {});
|
||||
});
|
||||
});
|
||||
@@ -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(), []);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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) + '/'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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')));
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user