2025-07-01 21:52:19 -03:00

408 lines
11 KiB
TypeScript

import assert from 'node:assert';
import { describe, it, mock } from 'node:test';
import { setImmediate } from 'node:timers/promises';
import Axios from 'axios';
import { setupCache } from '../../src/cache/create.js';
import { Header } from '../../src/header/headers.js';
import { mockAxios, XMockRandom } from '../mocks/axios.js';
describe('Response Interceptor', () => {
it('`storage.get` call without specified methods', async () => {
const axios = mockAxios({
// only cache post methods
methods: ['post']
});
const spy = mock.method(axios.storage, 'get');
await axios.get('http://test.com');
assert.equal(spy.mock.callCount(), 0);
});
it('`storage.get` call with specified methods', async () => {
const axios = mockAxios({
// only cache get methods
methods: ['get']
});
const spy = mock.method(axios.storage, 'get');
await axios.get('http://test.com');
assert.ok(spy.mock.callCount() >= 1);
});
it('Expects error for `storage.get` calls against specified methods', async () => {
const instance = Axios.create({});
const axios = setupCache(instance, {});
// only cache post methods
axios.defaults.cache.methods = ['post'];
const spy = mock.method(axios.storage, 'get');
try {
await axios.get('http://unknown.url.lan:1234');
assert.fail('should have thrown an error');
} catch {
assert.equal(spy.mock.callCount(), 0);
}
});
it('Expects error for `storage.get` call with specified methods', async () => {
const instance = Axios.create({});
const axios = setupCache(instance, {});
// only cache get methods
axios.defaults.cache.methods = ['get'];
const spy = mock.method(axios.storage, 'get');
try {
await axios.get('http://unknown.url.lan:1234');
assert.fail('should have thrown an error');
} catch (_error) {
assert.ok(spy.mock.callCount() >= 1);
}
});
it('CachePredicate integration', async () => {
const axios = mockAxios();
const fetch = () =>
axios.get('http://test.com', {
cache: {
cachePredicate: {
responseMatch: () => false
}
}
});
// Make first request to cache it
await fetch();
const result = await fetch();
assert.equal(result.cached, false);
assert.equal(result.stale, undefined);
});
it('HeaderInterpreter integration', async () => {
const axiosNoCache = mockAxios({}, { [Header.CacheControl]: 'no-cache' });
// Make first request to cache it
await axiosNoCache.get('http://test.com', { cache: { interpretHeader: true } });
const resultNoCache = await axiosNoCache.get('http://test.com');
assert.equal(resultNoCache.cached, false);
assert.equal(resultNoCache.stale, undefined);
const axiosCache = mockAxios({}, { [Header.CacheControl]: `max-age=${60 * 60 * 24 * 365}` });
// Make first request to cache it
await axiosCache.get('http://test.com', { cache: { interpretHeader: true } });
const resultCache = await axiosCache.get('http://test.com');
assert.ok(resultCache.cached);
assert.equal(resultCache.stale, false);
});
it('Update cache integration', async () => {
const axios = mockAxios();
const { id } = await axios.get('key01');
await axios.get('key02', {
cache: {
update: {
[id]: 'delete' as const
}
}
});
const cache = await axios.storage.get(id);
assert.equal(cache.state, 'empty');
});
it('Blank CacheControl header', async () => {
const defaultTtl = 60;
const axios = mockAxios(
{ ttl: defaultTtl, interpretHeader: true },
{ [Header.CacheControl]: '' }
);
const { id } = await axios.get('key01', {
cache: {
interpretHeader: true
}
});
const cache = await axios.storage.get(id);
assert.equal(cache.state, 'cached');
assert.equal(cache.ttl, defaultTtl);
});
it('TTL with functions', async () => {
const axios = mockAxios();
const id = 'my-id';
// first request (cached by tll)
await axios.get('url', {
id,
cache: {
ttl: (resp) => {
assert.equal(resp.cached, false);
assert.equal(resp.stale, undefined);
assert.ok(resp.config);
assert.notEqual(resp.headers[XMockRandom], Number.NaN);
assert.equal(resp.status, 200);
assert.equal(resp.statusText, '200 OK');
assert.ok(resp.data);
return 100;
}
}
});
const cache1 = await axios.storage.get(id);
assert.equal(cache1.state, 'cached');
assert.equal(cache1.ttl, 100);
// Second request (cached by ttl)
const ttl = mock.fn(() => 200);
await axios.get('url', {
id,
cache: { ttl }
});
const cache2 = await axios.storage.get(id);
assert.equal(cache2.state, 'cached');
assert.equal(cache2.ttl, 100);
assert.equal(ttl.mock.callCount(), 0);
// Force invalidation
await axios.storage.remove(id);
});
it('Async ttl function', async () => {
const axios = mockAxios();
// A lot of promises and callbacks
const { id } = await axios.get('url', {
cache: {
ttl: async () => {
await setImmediate(); // jumps to next nodejs event loop tick
return new Promise((res) => {
setTimeout(() => {
process.nextTick(() => {
res(173);
});
}, 20);
});
}
}
});
const cache = await axios.storage.get(id);
assert.equal(cache.state, 'cached');
assert.equal(cache.ttl, 173);
});
it('Ensures a request id has been generated even with `cache: false`', async () => {
const axios = mockAxios();
const { id } = await axios.get('url', { cache: false });
assert.ok(id);
assert.equal(typeof id, 'string');
});
it('Any X-axios-cache header gets removed', async () => {
const headerValue = '23asdf8ghd';
const axios = mockAxios(
{},
{
[Header.XAxiosCacheEtag]: headerValue,
[Header.XAxiosCacheLastModified]: headerValue,
[Header.XAxiosCacheStaleIfError]: headerValue
}
);
const { headers } = await axios.get('url');
assert.notEqual(headers[Header.XAxiosCacheEtag], headerValue);
assert.notEqual(headers[Header.XAxiosCacheLastModified], headerValue);
assert.notEqual(headers[Header.XAxiosCacheStaleIfError], headerValue);
});
// https://github.com/arthurfiorette/axios-cache-interceptor/issues/317
it('Aborted requests clears its cache afterwards', async () => {
const id = 'abort-request-id';
const ac = new AbortController();
const axios = mockAxios();
const promise = axios.get('url', { id, signal: ac.signal });
ac.abort();
await assert.rejects(promise, Error);
const cache = await axios.storage.get(id);
assert.notEqual(cache.state, 'loading');
assert.equal(cache.state, 'empty');
});
it('Response interceptor handles non response errors', async () => {
const instance = Axios.create();
const NOT_RESPONSE = { notAResponse: true };
//@ts-expect-error - this is indeed wrongly behavior
instance.interceptors.response.use(() => NOT_RESPONSE);
const axios = mockAxios(undefined, undefined, instance);
await assert.rejects(
// just calls the response interceptor
axios.get('url'),
NOT_RESPONSE
);
});
it('Works when modifying response', async () => {
const axios = mockAxios();
// fresh response from server and transformed
const freshResponse = await axios.get('url', {
transformResponse: (data: unknown) => [data]
});
// cached response
// should not transform again as already in desired format
const cachedResponse = await axios.get('url', {
transformResponse: (data: unknown) => [data]
});
assert.deepEqual(freshResponse.data, [true]);
assert.deepEqual(cachedResponse.data, [true]);
});
it('Works when modifying the error response', async () => {
const axios = mockAxios();
const error = new Error();
const promise = axios.get('url', {
transformResponse: () => {
throw error;
}
});
await assert.rejects(promise, error);
});
it('Cancelled deferred still should save cache after new response', async () => {
const axios = mockAxios();
const id = '1';
const controller = new AbortController();
const cancelled = axios.get('url', { id, signal: controller.signal });
const promise = axios.get('url', { id });
controller.abort();
// p1 should fail as it was aborted
try {
await cancelled;
assert.fail('should have thrown an error');
} catch (error: any) {
assert.equal(error.code, 'ERR_CANCELED');
}
const response = await promise;
// p2 should succeed as it was not aborted
await assert.ok(response.data);
await assert.equal(response.cached, false);
assert.equal(response.stale, undefined);
const storage = await axios.storage.get(id);
// P2 should have saved the cache
// even that his origin was from a cancelled deferred
assert.equal(storage.state, 'cached');
assert.equal(storage.data?.data, true);
});
it('Response gets cached even if there is a pending request without deferred.', async () => {
const axios = mockAxios();
const id = '1';
// Simulates previous unresolved request
await axios.storage.set(id, {
state: 'loading',
previous: 'empty'
});
const response = await axios.get('url', { id });
assert.equal(response.cached, false);
assert.equal(response.stale, undefined);
assert.ok(response.data);
const storage = await axios.storage.get(id);
assert.equal(storage.state, 'cached');
assert.equal(storage.data?.data, true);
});
// https://github.com/arthurfiorette/axios-cache-interceptor/issues/922
it('Aborted requests should preserve non-stale valid cache entries', async () => {
const instance = Axios.create({});
const axios = setupCache(instance, {});
const id = '1';
const cache = {
data: true,
headers: {},
status: 200,
statusText: 'Ok'
};
// Cache request
axios.storage.set(id, {
state: 'cached',
ttl: 5000,
createdAt: Date.now(),
data: cache
});
// First request cancelled immediately
const controller = new AbortController();
const cancelled = axios.get('http://unknown.url.lan:1234', { id, signal: controller.signal });
controller.abort();
try {
await cancelled;
assert.fail('should have thrown an error');
} catch (error: any) {
assert.equal(error.code, 'ERR_CANCELED');
}
// Second request cancelled after a macrotask
const controller2 = new AbortController();
const promise = axios.get('http://unknown.url.lan:1234', { id, signal: controller2.signal });
// Wait for eventual request to be sent
await new Promise((res) => setTimeout(res));
controller2.abort();
const response = await promise;
assert.ok(response.cached);
});
});