feat: add checks to support deno and bun (#10652)

* feat: added smoke tests for deno

* feat: added bun smoke tests

* chore: added workflows for deno and bun

* chore: swap workflow implementation

* chore: apply ai suggestion

* chore: test alt install of bun deps

* chore: deno install

* chore: map bun file install

* chore: try a different approach for bun

* chore: unpack and then install for bun

* chore: remove un-needed step

* chore: try with tgx again for bun

* chore: alternative zip approach

* ci: full ci added back
This commit is contained in:
Jay 2026-04-05 14:37:16 +02:00 committed by GitHub
parent 23fcd5f278
commit 2f52f6b13b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 2913 additions and 1 deletions

View File

@ -189,9 +189,64 @@ jobs:
working-directory: tests/module/esm working-directory: tests/module/esm
run: npm run test:module:esm run: npm run test:module:esm
bun-smoke-tests:
name: Bun smoke tests
needs: build-and-run-vitest
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
- name: Download npm pack artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: axios-tarball
path: artifacts
- name: Install packed axios
env:
TMPDIR: ${{ runner.temp }}
BUN_INSTALL_CACHE_DIR: ${{ runner.temp }}/bun-cache
run: |
mkdir -p "$BUN_INSTALL_CACHE_DIR"
mv artifacts/axios-*.tgz artifacts/axios.tgz
cd tests/smoke/bun
bun add file:../../../artifacts/axios.tgz
- name: Run Bun smoke tests
working-directory: tests/smoke/bun
run: bun test
deno-smoke-tests:
name: Deno smoke tests
needs: build-and-run-vitest
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup deno
uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
- name: Download npm pack artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: axios-tarball
path: artifacts
- name: Prepare packed axios dist
run: mkdir -p dist && tar -xzf artifacts/axios-*.tgz -C artifacts && cp -R artifacts/package/dist/. ./dist
- name: Install Deno smoke test dependencies
working-directory: tests/smoke/deno
run: deno install
- name: Run Deno smoke tests
working-directory: tests/smoke/deno
run: deno task test
bump-version-and-create-pr: bump-version-and-create-pr:
name: Bump version and create PR name: Bump version and create PR
needs: [build-and-run-vitest, cjs-smoke-tests, esm-smoke-tests] needs:
[build-and-run-vitest, cjs-smoke-tests, esm-smoke-tests, bun-smoke-tests, deno-smoke-tests]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write

View File

@ -182,3 +182,57 @@ jobs:
- name: Run ESM module tests - name: Run ESM module tests
working-directory: tests/module/esm working-directory: tests/module/esm
run: npm run test:module:esm run: npm run test:module:esm
bun-smoke-tests:
name: Bun smoke tests
needs: build-and-run-vitest
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
- name: Download npm pack artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: axios-tarball
path: artifacts
- name: Install packed axios
env:
TMPDIR: ${{ runner.temp }}
BUN_INSTALL_CACHE_DIR: ${{ runner.temp }}/bun-cache
run: |
mkdir -p "$BUN_INSTALL_CACHE_DIR"
mv artifacts/axios-*.tgz artifacts/axios.tgz
cd tests/smoke/bun
bun add file:../../../artifacts/axios.tgz
- name: Run Bun smoke tests
working-directory: tests/smoke/bun
run: bun test
deno-smoke-tests:
name: Deno smoke tests
needs: build-and-run-vitest
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup deno
uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
- name: Download npm pack artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: axios-tarball
path: artifacts
- name: Prepare packed axios dist
run: mkdir -p dist && tar -xzf artifacts/axios-*.tgz -C artifacts && cp -R artifacts/package/dist/. ./dist
- name: Install Deno smoke test dependencies
working-directory: tests/smoke/deno
run: deno install
- name: Run Deno smoke tests
working-directory: tests/smoke/deno
run: deno task test

1
.gitignore vendored
View File

@ -14,5 +14,6 @@ backup/
dist/ dist/
.vscode/ .vscode/
openspec/ openspec/
.opencode/
docs/.vitepress/dist docs/.vitepress/dist
docs/.vitepress/cache docs/.vitepress/cache

View File

@ -109,6 +109,8 @@
"test:vitest:watch": "vitest", "test:vitest:watch": "vitest",
"test:smoke:cjs:vitest": "npm --prefix tests/smoke/cjs run test:smoke:cjs:mocha", "test:smoke:cjs:vitest": "npm --prefix tests/smoke/cjs run test:smoke:cjs:mocha",
"test:smoke:esm:vitest": "npm --prefix tests/smoke/esm run test:smoke:esm:vitest", "test:smoke:esm:vitest": "npm --prefix tests/smoke/esm run test:smoke:esm:vitest",
"test:smoke:deno": "deno task --cwd tests/smoke/deno test",
"test:smoke:bun": "bun test --cwd tests/smoke/bun",
"test:module:cjs": "npm --prefix tests/module/cjs run test:module:cjs", "test:module:cjs": "npm --prefix tests/module/cjs run test:module:cjs",
"test:module:esm": "npm --prefix tests/module/esm run test:module:esm", "test:module:esm": "npm --prefix tests/module/esm run test:module:esm",
"docs:dev": "cd docs && npm run docs:dev", "docs:dev": "cd docs && npm run docs:dev",

1695
tests/smoke/bun/bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
{
"name": "@axios/bun-smoke-tests",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
}
}

View File

@ -0,0 +1,84 @@
import { describe, expect, test } from 'bun:test';
import axios from 'axios';
const env = (fetch: typeof globalThis.fetch) => ({
fetch,
Request,
Response,
});
describe('cancellation', () => {
test('pre-aborted AbortController cancels before fetch is called', async () => {
let fetchCallCount = 0;
const fetch = async () => {
fetchCallCount += 1;
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
const controller = new AbortController();
controller.abort();
const err = await axios
.get('https://example.com/cancel', {
adapter: 'fetch',
signal: controller.signal,
env: env(fetch),
})
.catch((e: any) => e);
expect(axios.isCancel(err)).toBe(true);
expect(err.code).toBe('ERR_CANCELED');
expect(fetchCallCount).toBe(0);
});
test('in-flight AbortController abort cancels the request', async () => {
const fetch = (_input: unknown, init?: RequestInit) =>
new Promise<Response>((_resolve, reject) => {
const abortError = () =>
reject(new DOMException('The operation was aborted', 'AbortError'));
const timeout = setTimeout(abortError, 20);
if (init?.signal) {
if (init.signal.aborted) {
clearTimeout(timeout);
abortError();
return;
}
init.signal.addEventListener(
'abort',
() => {
clearTimeout(timeout);
abortError();
},
{ once: true }
);
}
});
const controller = new AbortController();
const request = axios.get('https://example.com/in-flight', {
adapter: 'fetch',
signal: controller.signal,
env: env(fetch),
});
controller.abort();
const err = await request.catch((e: any) => e);
expect(axios.isCancel(err)).toBe(true);
expect(err.code).toBe('ERR_CANCELED');
});
test('axios.isCancel returns false for a plain Error', () => {
expect(axios.isCancel(new Error('random'))).toBe(false);
});
});

View File

@ -0,0 +1,33 @@
import { describe, expect, test } from 'bun:test';
import axios from 'axios';
const env = (fetch: typeof globalThis.fetch) => ({
fetch,
Request,
Response,
});
describe('errors', () => {
test('non-2xx response rejects with AxiosError and status 404', async () => {
const fetch = async () =>
new Response(JSON.stringify({ error: 'missing' }), {
status: 404,
statusText: 'Not Found',
headers: { 'Content-Type': 'application/json' },
});
const err = await axios
.get('https://example.com/missing', {
adapter: 'fetch',
env: env(fetch),
})
.catch((e: any) => e);
expect(axios.isAxiosError(err)).toBe(true);
expect(err.response.status).toBe(404);
});
test('axios.isAxiosError returns false for a plain Error', () => {
expect(axios.isAxiosError(new Error('plain'))).toBe(false);
});
});

View File

@ -0,0 +1,126 @@
import { describe, expect, test } from 'bun:test';
import axios from 'axios';
const createFetchMock = (
responseFactory?: (input: unknown, init: RequestInit) => Response | Promise<Response>
) => {
const calls: Array<{ input: unknown; init: RequestInit }> = [];
const mockFetch = async (input: unknown, init: RequestInit = {}) => {
calls.push({ input, init });
if (responseFactory) {
return responseFactory(input, init);
}
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
return {
mockFetch,
getCalls: () => calls,
};
};
const getRequestMeta = async (input: unknown, init: RequestInit = {}) => {
const request = input instanceof Request ? input : new Request(input as string, init);
return {
url: request.url,
method: request.method,
body:
request.method === 'GET' || request.method === 'HEAD'
? undefined
: await request.clone().text(),
};
};
const env = (fetch: typeof globalThis.fetch) => ({
fetch,
Request,
Response,
});
describe('fetch adapter', () => {
test('GET resolves JSON response via fetch adapter', async () => {
const { mockFetch, getCalls } = createFetchMock();
const response = await axios.get('https://example.com/users', {
adapter: 'fetch',
env: env(mockFetch),
});
expect(response.status).toBe(200);
expect(response.data).toEqual({ ok: true });
expect(getCalls()).toHaveLength(1);
});
test('POST serializes JSON body via fetch adapter', async () => {
const { mockFetch, getCalls } = createFetchMock();
await axios.post(
'https://example.com/items',
{ name: 'widget' },
{
adapter: 'fetch',
env: env(mockFetch),
}
);
const { input, init } = getCalls()[0];
const meta = await getRequestMeta(input, init);
expect(meta.body).toBe(JSON.stringify({ name: 'widget' }));
});
test('HTTP methods are forwarded correctly', async () => {
const run = async (
method: 'delete' | 'head' | 'options' | 'put' | 'patch',
expected: string
) => {
const { mockFetch, getCalls } = createFetchMock();
if (method === 'put' || method === 'patch') {
await axios[method](
'https://example.com/items',
{ name: 'widget' },
{
adapter: 'fetch',
env: env(mockFetch),
}
);
} else {
await axios[method]('https://example.com/items', {
adapter: 'fetch',
env: env(mockFetch),
});
}
const { input, init } = getCalls()[0];
const meta = await getRequestMeta(input, init);
expect(meta.method).toBe(expected);
};
await run('delete', 'DELETE');
await run('head', 'HEAD');
await run('options', 'OPTIONS');
await run('put', 'PUT');
await run('patch', 'PATCH');
});
test('full URL is preserved in the fetch request', async () => {
const { mockFetch, getCalls } = createFetchMock();
await axios.get('https://example.com/users', {
adapter: 'fetch',
env: env(mockFetch),
});
const { input, init } = getCalls()[0];
const meta = await getRequestMeta(input, init);
expect(meta.url).toBe('https://example.com/users');
});
});

View File

@ -0,0 +1,110 @@
import { describe, expect, test } from 'bun:test';
import { PassThrough, Writable } from 'node:stream';
import FormDataPackage from 'form-data';
import axios from 'axios';
const createTransportMock = (
responseFactory?: (body: Buffer, options: Record<string, any>) => Record<string, any>
) => {
const transport = {
request(options: Record<string, any>, onResponse: (res: PassThrough) => void) {
const chunks: Buffer[] = [];
const req = new Writable({
write(chunk, _encoding, callback) {
chunks.push(Buffer.from(chunk));
callback();
},
}) as Writable & Record<string, any>;
req.destroyed = false;
req.setTimeout = () => {};
req.write = req.write.bind(req);
req.destroy = () => {
req.destroyed = true;
return req;
};
req.close = req.destroy;
const originalEnd = req.end.bind(req);
req.end = (...args: unknown[]) => {
originalEnd(...(args as Parameters<Writable['end']>));
const body = Buffer.concat(chunks);
const response = responseFactory ? responseFactory(body, options) : {};
const res = new PassThrough() as PassThrough & Record<string, any>;
res.statusCode = response.statusCode ?? 200;
res.statusMessage = response.statusMessage ?? 'OK';
res.headers = response.headers ?? { 'content-type': 'application/json' };
res.req = req;
onResponse(res);
res.end(response.body ?? JSON.stringify({ ok: true }));
return req;
};
return req;
},
};
return { transport };
};
const bodyAsUtf8 = (value: unknown) => {
return Buffer.isBuffer(value) ? value.toString('utf8') : String(value);
};
describe('form data', () => {
test('native Bun FormData body produces multipart/form-data content-type', async () => {
const form = new FormData();
form.append('username', 'janedoe');
form.append('role', 'admin');
const { transport } = createTransportMock((body, options) => ({
body: JSON.stringify({
contentType:
options.headers && (options.headers['Content-Type'] || options.headers['content-type']),
payload: bodyAsUtf8(body),
}),
}));
const response = await axios.post('http://example.com/form', form, {
adapter: 'http',
proxy: false,
transport,
});
expect(response.data.contentType).toContain('multipart/form-data');
expect(response.data.payload).toContain('name="username"');
expect(response.data.payload).toContain('janedoe');
expect(response.data.payload).toContain('name="role"');
expect(response.data.payload).toContain('admin');
});
test('npm form-data package instance is serialized correctly', async () => {
const form = new FormDataPackage();
form.append('project', 'axios');
form.append('mode', 'compat');
const { transport } = createTransportMock((body, options) => ({
body: JSON.stringify({
contentType:
options.headers && (options.headers['Content-Type'] || options.headers['content-type']),
payload: bodyAsUtf8(body),
}),
}));
const response = await axios.post('http://example.com/npm-form-data', form as any, {
adapter: 'http',
proxy: false,
transport,
});
expect(response.data.contentType).toContain('multipart/form-data');
expect(response.data.payload).toContain('name="project"');
expect(response.data.payload).toContain('axios');
expect(response.data.payload).toContain('name="mode"');
expect(response.data.payload).toContain('compat');
});
});

View File

@ -0,0 +1,62 @@
import { describe, expect, test } from 'bun:test';
import axios from 'axios';
const createFetchCapture = () => {
const calls: Request[] = [];
const fetch = async (input: unknown, init?: RequestInit) => {
const request = input instanceof Request ? input : new Request(input as string, init);
calls.push(request);
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
return {
fetch,
getCalls: () => calls,
};
};
const env = (fetch: typeof globalThis.fetch) => ({
fetch,
Request,
Response,
});
describe('headers', () => {
test('custom X-Custom header is forwarded to mock fetch (case-insensitive)', async () => {
const { fetch, getCalls } = createFetchCapture();
await axios.get('https://example.com/custom-headers', {
adapter: 'fetch',
headers: {
'X-Custom': 'trace-123',
},
env: env(fetch),
});
const request = getCalls()[0];
expect(request.headers.get('x-custom')).toBe('trace-123');
});
test('content-type application/json is inferred for JSON POST body', async () => {
const { fetch, getCalls } = createFetchCapture();
await axios.post(
'https://example.com/post-json',
{ name: 'widget' },
{
adapter: 'fetch',
env: env(fetch),
}
);
const request = getCalls()[0];
const contentType = request.headers.get('content-type') || '';
expect(contentType.includes('application/json')).toBe(true);
});
});

View File

@ -0,0 +1,104 @@
import { describe, expect, test } from 'bun:test';
import { EventEmitter } from 'node:events';
import { PassThrough } from 'node:stream';
import axios from 'axios';
type TransportCall = {
options: Record<string, any>;
body: Buffer;
};
const createTransportMock = (
responseFactory?: (body: Buffer, options: Record<string, any>) => Record<string, any>
) => {
const calls: TransportCall[] = [];
const transport = {
request(options: Record<string, any>, onResponse: (res: PassThrough) => void) {
const req = new EventEmitter() as Record<string, any>;
const chunks: Buffer[] = [];
req.destroyed = false;
req.setTimeout = () => {};
req.write = (chunk?: unknown) => {
if (chunk !== undefined) {
chunks.push(Buffer.from(chunk as string));
}
return true;
};
req.destroy = () => {
req.destroyed = true;
};
req.close = req.destroy;
req.end = (chunk?: unknown) => {
if (chunk !== undefined) {
chunks.push(Buffer.from(chunk as string));
}
const body = Buffer.concat(chunks);
calls.push({ options, body });
const response = responseFactory ? responseFactory(body, options) : {};
const res = new PassThrough() as PassThrough & Record<string, any>;
res.statusCode = response.statusCode ?? 200;
res.statusMessage = response.statusMessage ?? 'OK';
res.headers = response.headers ?? { 'content-type': 'application/json' };
res.req = req;
onResponse(res);
res.end(response.body ?? JSON.stringify({ ok: true }));
};
return req;
},
};
return {
transport,
getCalls: () => calls,
};
};
describe('http adapter', () => {
test('GET via http adapter returns mocked response data', async () => {
const { transport, getCalls } = createTransportMock();
const response = await axios.get('http://example.com/users', {
adapter: 'http',
proxy: false,
transport,
});
expect(response.status).toBe(200);
expect(response.data).toEqual({ ok: true });
expect(getCalls()).toHaveLength(1);
});
test('POST sends JSON-serialized body via http adapter', async () => {
const { transport, getCalls } = createTransportMock();
await axios.post(
'http://example.com/items',
{ name: 'widget' },
{
adapter: 'http',
proxy: false,
transport,
}
);
const { body } = getCalls()[0];
expect(body.toString('utf8')).toBe(JSON.stringify({ name: 'widget' }));
});
test('default adapter selection in Bun routes through http adapter', async () => {
const { transport, getCalls } = createTransportMock();
await axios.get('http://example.com/default-adapter', {
proxy: false,
transport,
});
expect(getCalls()).toHaveLength(1);
});
});

View File

@ -0,0 +1,19 @@
import { describe, expect, test } from 'bun:test';
import axios from 'axios';
describe('Bun importing', () => {
test('default export is callable', () => {
expect(typeof axios).toBe('function');
});
test('named exports are present', async () => {
const exports = (await import('axios')) as Record<string, any>;
expect(typeof (exports.axios ?? exports.default)).toBe('function');
expect(typeof (exports.create ?? exports.default.create)).toBe('function');
expect(typeof exports.isCancel).toBe('function');
expect(typeof exports.isAxiosError).toBe('function');
expect(typeof exports.CancelToken).toBe('function');
expect(typeof exports.VERSION).toBe('string');
});
});

View File

@ -0,0 +1,65 @@
import { describe, expect, test } from 'bun:test';
import axios from 'axios';
const createFetchCapture = () => {
const calls: Request[] = [];
const fetch = async (input: unknown, init?: RequestInit) => {
const request = input instanceof Request ? input : new Request(input as string, init);
calls.push(request);
return new Response(JSON.stringify({ value: 'ok' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
return {
fetch,
getCalls: () => calls,
};
};
const env = (fetch: typeof globalThis.fetch) => ({
fetch,
Request,
Response,
});
describe('interceptors', () => {
test('request interceptor header is forwarded to fetch', async () => {
const { fetch, getCalls } = createFetchCapture();
const client = axios.create({
adapter: 'fetch',
env: env(fetch),
});
client.interceptors.request.use((config: any) => {
config.headers = config.headers || {};
config.headers['X-Added'] = 'yes';
return config;
});
await client.get('https://example.com/interceptor-request');
expect(getCalls()).toHaveLength(1);
expect(getCalls()[0].headers.get('x-added')).toBe('yes');
});
test('response interceptor transform is reflected in resolved value', async () => {
const { fetch } = createFetchCapture();
const client = axios.create({
adapter: 'fetch',
env: env(fetch),
});
client.interceptors.response.use((response: any) => {
response.data.value = String(response.data.value).toUpperCase();
return response;
});
const response = await client.get('https://example.com/interceptor-response');
expect(response.data).toEqual({ value: 'OK' });
});
});

View File

@ -0,0 +1,45 @@
import { describe, expect, test } from 'bun:test';
import axios from 'axios';
const env = (fetch: typeof globalThis.fetch) => ({
fetch,
Request,
Response,
});
describe('progress', () => {
test('onDownloadProgress fires with loaded > 0 for streaming fetch response', async () => {
const samples: number[] = [];
const fetch = async () => {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('ab'));
controller.enqueue(new TextEncoder().encode('cd'));
controller.close();
},
});
return new Response(stream, {
status: 200,
headers: {
'Content-Type': 'text/plain',
'Content-Length': '4',
},
});
};
const response = await axios.get('https://example.com/download', {
adapter: 'fetch',
responseType: 'text',
onDownloadProgress: ({ loaded }: { loaded: number }) => {
samples.push(loaded);
},
env: env(fetch),
});
expect(response.data).toBe('abcd');
expect(samples.length).toBeGreaterThan(0);
expect(samples.some((loaded) => loaded > 0)).toBe(true);
});
});

View File

@ -0,0 +1,50 @@
import { describe, expect, test } from 'bun:test';
import axios from 'axios';
const env = (fetch: typeof globalThis.fetch) => ({
fetch,
Request,
Response,
});
const createAbortedError = () => {
const error = new Error('The operation was aborted') as Error & { code?: string; name: string };
error.name = 'AbortError';
error.code = 'ECONNABORTED';
return error;
};
describe('timeout', () => {
test('timeout: 50 with never-resolving fetch mock rejects with ECONNABORTED', async () => {
const fetch = (input: unknown, init?: RequestInit) =>
new Promise<Response>((_resolve, reject) => {
const signal = init?.signal || (input instanceof Request ? input.signal : undefined);
if (signal) {
if (signal.aborted) {
reject(createAbortedError());
return;
}
signal.addEventListener(
'abort',
() => {
reject(createAbortedError());
},
{ once: true }
);
}
});
const err = await axios
.get('https://example.com/timeout', {
adapter: 'fetch',
timeout: 50,
env: env(fetch),
})
.catch((e: any) => e);
expect(axios.isAxiosError(err)).toBe(true);
expect(err.code).toBe('ECONNABORTED');
});
});

View File

@ -0,0 +1,12 @@
{
"imports": {
"@std/assert": "jsr:@std/assert@1.0.19",
"axios": "../../../dist/esm/axios.js"
},
"tasks": {
"test": "deno test --allow-read tests/"
},
"test": {
"include": ["tests/**/*.smoke.test.ts"]
}
}

23
tests/smoke/deno/deno.lock generated Normal file
View File

@ -0,0 +1,23 @@
{
"version": "5",
"specifiers": {
"jsr:@std/assert@1.0.19": "1.0.19",
"jsr:@std/internal@^1.0.12": "1.0.12"
},
"jsr": {
"@std/assert@1.0.19": {
"integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e",
"dependencies": [
"jsr:@std/internal"
]
},
"@std/internal@1.0.12": {
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
}
},
"workspace": {
"dependencies": [
"jsr:@std/assert@1.0.19"
]
}
}

View File

@ -0,0 +1,81 @@
import { assertEquals } from '@std/assert';
import axios from 'axios';
const env = (fetch: any) => ({
fetch,
Request,
Response,
});
Deno.test('cancel: pre-aborted AbortController cancels request', async () => {
let fetchCallCount = 0;
const fetch = async () => {
fetchCallCount += 1;
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
const controller = new AbortController();
controller.abort();
const err = await axios
.get('https://example.com/cancel', {
adapter: 'fetch',
signal: controller.signal,
env: env(fetch),
})
.catch((e: any) => e);
assertEquals(axios.isCancel(err), true);
assertEquals(err.code, 'ERR_CANCELED');
assertEquals(fetchCallCount, 0);
});
Deno.test('cancel: in-flight abort cancels request', async () => {
const fetch = (_input: any, init?: any) =>
new Promise<Response>((_resolve, reject) => {
const timeout = setTimeout(() => {
reject(new DOMException('The operation was aborted', 'AbortError'));
}, 20);
if (init?.signal) {
if (init.signal.aborted) {
clearTimeout(timeout);
reject(new DOMException('The operation was aborted', 'AbortError'));
return;
}
init.signal.addEventListener(
'abort',
() => {
clearTimeout(timeout);
reject(new DOMException('The operation was aborted', 'AbortError'));
},
{ once: true }
);
}
});
const controller = new AbortController();
const request = axios.get('https://example.com/in-flight', {
adapter: 'fetch',
signal: controller.signal,
env: env(fetch),
});
controller.abort();
const err = await request.catch((e: any) => e);
assertEquals(axios.isCancel(err), true);
assertEquals(err.code, 'ERR_CANCELED');
});
Deno.test('cancel: isCancel returns false for plain Error', () => {
assertEquals(axios.isCancel(new Error('random')), false);
});

View File

@ -0,0 +1,51 @@
import { assertEquals } from '@std/assert';
import axios from 'axios';
const env = (fetch: any) => ({
fetch,
Request,
Response,
});
Deno.test('errors: rejects with AxiosError for 500', async () => {
const fetch = async () =>
new Response(JSON.stringify({ error: 'boom' }), {
status: 500,
statusText: 'Internal Server Error',
headers: { 'Content-Type': 'application/json' },
});
const err = await axios
.get('https://example.com/fail', {
adapter: 'fetch',
env: env(fetch),
})
.catch((e: any) => e);
assertEquals(axios.isAxiosError(err), true);
assertEquals(err.response.status, 500);
assertEquals(err.response.data, { error: 'boom' });
});
Deno.test('errors: rejects with AxiosError for 404', async () => {
const fetch = async () =>
new Response(JSON.stringify({ error: 'missing' }), {
status: 404,
statusText: 'Not Found',
headers: { 'Content-Type': 'application/json' },
});
const err = await axios
.get('https://example.com/missing', {
adapter: 'fetch',
env: env(fetch),
})
.catch((e: any) => e);
assertEquals(axios.isAxiosError(err), true);
assertEquals(err.response.status, 404);
});
Deno.test('errors: isAxiosError returns false for plain Error', () => {
assertEquals(axios.isAxiosError(new Error('plain')), false);
});

View File

@ -0,0 +1,126 @@
import { assertEquals } from '@std/assert';
import axios from 'axios';
const createFetchMock = (
responseFactory?: (input: any, init: any) => Response | Promise<Response>
) => {
const calls: Array<{ input: any; init: any }> = [];
const mockFetch = async (input: any, init: any = {}) => {
calls.push({ input, init });
if (responseFactory) {
return responseFactory(input, init);
}
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
return {
mockFetch,
getCalls: () => calls,
};
};
const getRequestMeta = async (input: any, init: any) => {
const request = input instanceof Request ? input : new Request(input, init);
return {
url: request.url,
method: request.method,
body:
request.method === 'GET' || request.method === 'HEAD'
? undefined
: await request.clone().text(),
};
};
const env = (fetch: any) => ({
fetch,
Request,
Response,
});
Deno.test('fetch adapter: GET resolves JSON response', async () => {
const { mockFetch, getCalls } = createFetchMock();
const response = await axios.get('https://example.com/users', {
adapter: 'fetch',
env: env(mockFetch),
});
assertEquals(response.status, 200);
assertEquals(response.data, { ok: true });
assertEquals(getCalls().length, 1);
});
Deno.test('fetch adapter: forwards HTTP methods', async () => {
const run = async (
method: 'delete' | 'head' | 'options' | 'post' | 'put' | 'patch',
expected: string
) => {
const { mockFetch, getCalls } = createFetchMock();
if (method === 'post' || method === 'put' || method === 'patch') {
await axios[method](
'https://example.com/items',
{ name: 'widget' },
{
adapter: 'fetch',
env: env(mockFetch),
}
);
} else {
await axios[method]('https://example.com/items', {
adapter: 'fetch',
env: env(mockFetch),
});
}
const { input, init } = getCalls()[0];
const meta = await getRequestMeta(input, init);
assertEquals(meta.method, expected);
};
await run('delete', 'DELETE');
await run('head', 'HEAD');
await run('options', 'OPTIONS');
await run('post', 'POST');
await run('put', 'PUT');
await run('patch', 'PATCH');
});
Deno.test('fetch adapter: serializes JSON body for POST', async () => {
const { mockFetch, getCalls } = createFetchMock();
await axios.post(
'https://example.com/items',
{ name: 'widget' },
{
adapter: 'fetch',
env: env(mockFetch),
}
);
const { input, init } = getCalls()[0];
const meta = await getRequestMeta(input, init);
assertEquals(meta.body, JSON.stringify({ name: 'widget' }));
});
Deno.test('fetch adapter: forwards full URL', async () => {
const { mockFetch, getCalls } = createFetchMock();
await axios.get('https://example.com/users', {
adapter: 'fetch',
env: env(mockFetch),
});
const { input, init } = getCalls()[0];
const meta = await getRequestMeta(input, init);
assertEquals(meta.url, 'https://example.com/users');
});

View File

@ -0,0 +1,85 @@
import { assertEquals } from '@std/assert';
import axios from 'axios';
const createFetchCapture = () => {
const calls: Request[] = [];
const fetch = async (input: any, init?: any) => {
const request = input instanceof Request ? input : new Request(input, init);
calls.push(request);
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
return {
fetch,
getCalls: () => calls,
};
};
const env = (fetch: any) => ({
fetch,
Request,
Response,
});
Deno.test('headers: default Accept header is sent', async () => {
const { fetch, getCalls } = createFetchCapture();
await axios.get('https://example.com/default-headers', {
adapter: 'fetch',
env: env(fetch),
});
const request = getCalls()[0];
assertEquals(request.headers.get('accept'), 'application/json, text/plain, */*');
});
Deno.test('headers: custom headers are forwarded', async () => {
const { fetch, getCalls } = createFetchCapture();
await axios.get('https://example.com/custom-headers', {
adapter: 'fetch',
headers: {
'X-Trace-Id': 'trace-123',
Authorization: 'Bearer token-abc',
},
env: env(fetch),
});
const request = getCalls()[0];
assertEquals(request.headers.get('x-trace-id'), 'trace-123');
assertEquals(request.headers.get('authorization'), 'Bearer token-abc');
});
Deno.test('headers: content-type is set for JSON POST payload', async () => {
const { fetch, getCalls } = createFetchCapture();
await axios.post(
'https://example.com/post-json',
{ name: 'widget' },
{
adapter: 'fetch',
env: env(fetch),
}
);
const request = getCalls()[0];
const contentType = request.headers.get('content-type') || '';
assertEquals(contentType.includes('application/json'), true);
});
Deno.test('headers: content-type is absent for bodyless GET', async () => {
const { fetch, getCalls } = createFetchCapture();
await axios.get('https://example.com/get-no-body', {
adapter: 'fetch',
env: env(fetch),
});
const request = getCalls()[0];
assertEquals(request.headers.get('content-type'), null);
});

View File

@ -0,0 +1,18 @@
import { assertEquals } from '@std/assert';
import axios, { AxiosError, AxiosHeaders, CanceledError } from 'axios';
Deno.test('Deno importing: default export is callable', () => {
assertEquals(typeof axios, 'function');
});
Deno.test('Deno importing: named exports are functions', () => {
assertEquals(typeof AxiosError, 'function');
assertEquals(typeof CanceledError, 'function');
assertEquals(typeof AxiosHeaders, 'function');
});
Deno.test('Deno importing: named exports match axios properties', () => {
assertEquals(axios.AxiosError, AxiosError);
assertEquals(axios.CanceledError, CanceledError);
assertEquals(axios.AxiosHeaders, AxiosHeaders);
});