import type { AxiosAdapter, AxiosResponse } from 'axios'; import { setTimeout } from 'timers/promises'; import type { LoadingStorageValue } from '../../src'; import type { CacheRequestConfig } from '../../src/cache/axios'; import { mockAxios } from '../mocks/axios'; import { sleep } from '../utils'; describe('test request interceptor', () => { it('tests against specified methods', async () => { const axios = mockAxios({ // only cache post methods methods: ['post'] }); const response = await axios.get('http://test.com'); const cacheKey = axios.generateKey(response.config); const cache = await axios.storage.get(cacheKey); expect(cache.state).toBe('empty'); }); it('tests specified methods', async () => { const axios = mockAxios({ // only cache get methods methods: ['get'] }); const response = await axios.get('http://test.com'); const cacheKey = axios.generateKey(response.config); const cache = await axios.storage.get(cacheKey); expect(cache.state).toBe('cached'); }); it('tests concurrent requests', async () => { const axios = mockAxios(); const [resp1, resp2] = await Promise.all([ axios.get('http://test.com'), axios.get('http://test.com') ]); expect(resp1.cached).toBe(false); expect(resp2.cached).toBe(true); }); it('tests concurrent requests with cache: false', async () => { const axios = mockAxios(); const results = await Promise.all([ axios.get('http://test.com', { cache: false }), axios.get('http://test.com'), axios.get('http://test.com', { cache: false }) ]); for (const result of results) { expect(result.cached).toBe(false); } }); /** * This is to test when two requests are made simultaneously. With that, the second * response waits the deferred from the first one. Because the first request is not * cached, the second should not be waiting forever for the deferred to be resolved with * a cached response. */ it('tests concurrent requests with uncached responses', async () => { const axios = mockAxios(); const [, resp2] = await Promise.all([ axios.get('http://test.com', { // Simple predicate to ignore cache in the response step. cache: { cachePredicate: () => false } }), axios.get('http://test.com') ]); expect(resp2.cached).toBe(false); }); it('tests response.cached', async () => { const axios = mockAxios(); const response = await axios.get('http://test.com'); expect(response.cached).toBe(false); const response2 = await axios.get('http://test.com'); expect(response2.cached).toBe(true); const response3 = await axios.get('http://test.com', { id: 'random-id' }); expect(response3.cached).toBe(false); const response4 = await axios.get('http://test.com', { id: 'random-id' }); expect(response4.cached).toBe(true); }); it('test cache expiration', async () => { const axios = mockAxios({}, { 'cache-control': 'max-age=1' }); await axios.get('http://test.com', { cache: { interpretHeader: true } }); const resultCache = await axios.get('http://test.com'); expect(resultCache.cached).toBe(true); // Sleep entire max age time. await sleep(1000); const response2 = await axios.get('http://test.com'); expect(response2.cached).toBe(false); }); it('tests "must revalidate" handling without any headers to do so', async () => { const axios = mockAxios({}, { 'cache-control': 'must-revalidate' }); const config = { cache: { interpretHeader: true } }; await axios.get('http://test.com', config); // 0ms cache await sleep(1); const response = await axios.get('http://test.com', config); // nothing to use for revalidation expect(response.cached).toBe(false); }); it("expect two requests with different body aren't cached", async () => { const axios = mockAxios(); const result = await axios.get('url', { data: { a: 1 } }); expect(result.cached).toBe(false); const result2 = await axios.get('url', { data: { a: 2 } }); expect(result2.cached).toBe(false); }); it('tests a request with really long keys', async () => { const axios = mockAxios(); const result = await axios.get('url', { data: Array(5e3).fill({ rnd: Math.random() }), params: Array(5e3).fill({ rnd: Math.random() }) }); expect(result).toBeDefined(); }); it('expect keyGenerator is called once during a single request', async () => { const axios = mockAxios(); const spy = jest.spyOn(axios, 'generateKey'); await axios.get('url', { // generates a long key data: Array(5e3).fill({ rnd: Math.random() }) }); expect(spy).toHaveBeenCalledTimes(1); }); it('Deleting a cache in the middle of a request should be fine', async () => { const ID = 'custom-id'; const axios = mockAxios(); // eslint-disable-next-line @typescript-eslint/no-floating-promises await axios.get('url', { id: ID, // A simple adapter that deletes the current cache // before resolving the adapter. Simulates when a user // manually deletes this key before it can be resolved. adapter: async (config: CacheRequestConfig) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await axios.storage.remove(config.id!); return (axios.defaults.adapter as AxiosAdapter)(config); } }); // Expects that the cache is empty (deleted above) and // it still has a waiting entry. const { state } = await axios.storage.get(ID); expect(state).toBe('empty'); expect(axios.waiting[ID]).toBeDefined(); // This line should throw an error if this bug isn't fixed. await axios.get('url', { id: ID }); const { state: newState } = await axios.storage.get(ID); expect(newState).not.toBe('empty'); expect(axios.waiting[ID]).toBeUndefined(); }); it('tests cache.override = true with previous cache', async () => { const axios = mockAxios(); // First normal request to populate cache const { id, ...initialResponse } = await axios.get('url'); expect(initialResponse.cached).toBe(false); // Ensure cache was populated const c1 = await axios.storage.get(id); expect(c1.state).toBe('cached'); // Make a request with cache.override = true const promise = axios.get('url', { id, cache: { override: true }, // Simple adapter that resolves after the deferred is completed. adapter: async (config: CacheRequestConfig) => { await setTimeout(150); const response = (await (axios.defaults.adapter as AxiosAdapter)( config )) as AxiosResponse; // Changes the response to be different from `true` (default) response.data = 'overridden response'; return response; } }); // These two setTimeouts is to ensure this code is executed after // the request interceptor, but before the response interceptor. // Leading to test the intermediate loading state. { await setTimeout(50); const c2 = (await axios.storage.get(id)) as LoadingStorageValue; expect(c2.state).toBe('loading'); expect(c2.previous).toBe('stale'); expect(c2.data).toBe(c1.data); expect(c2.createdAt).toBe(c1.createdAt); } // Waits for the promise completion const newResponse = await promise; // This step is after the cache was updated with the new response. { const c3 = await axios.storage.get(id); expect(newResponse.cached).toBe(false); expect(c3.state).toBe('cached'); expect(c3.data).not.toBe(c1.data); // `'overridden response'`, not `true` expect(c3.createdAt).not.toBe(c1.createdAt); } }); it('tests cache.override = true without previous cache', async () => { const id = 'CUSTOM_RANDOM_ID'; const axios = mockAxios(); const c1 = await axios.storage.get(id); expect(c1.state).toBe('empty'); // Make a request with cache.override = true const promise = axios.get('url', { id, cache: { override: true }, // Simple adapter that resolves after the deferred is completed. adapter: async (config: CacheRequestConfig) => { await setTimeout(150); return (axios.defaults.adapter as AxiosAdapter)(config); } }); // These two setTimeouts is to ensure this code is executed after // the request interceptor, but before the response interceptor. // Leading to test the intermediate loading state. { await setTimeout(50); const c2 = (await axios.storage.get(id)) as LoadingStorageValue; expect(c2.state).toBe('loading'); expect(c2.previous).toBe('empty'); expect(c2.data).toBeUndefined(); expect(c2.createdAt).not.toBe(c1.createdAt); } // Waits for the promise completion const newResponse = await promise; // This step is after the cache was updated with the new response. { const c3 = await axios.storage.get(id); expect(newResponse.cached).toBe(false); expect(c3.state).toBe('cached'); expect(c3.data).not.toBeUndefined(); expect(c3.createdAt).not.toBe(c1.createdAt); } }); });