diff --git a/test/cache-interceptor/cache-store-test-utils.js b/test/cache-interceptor/cache-store-test-utils.js index 74f10a0da37..c956902ab81 100644 --- a/test/cache-interceptor/cache-store-test-utils.js +++ b/test/cache-interceptor/cache-store-test-utils.js @@ -1,9 +1,10 @@ 'use strict' -const { describe, test } = require('node:test') -const { deepStrictEqual, notEqual, equal } = require('node:assert') +const { equal, notEqual, deepStrictEqual } = require('node:assert') +const { describe, test, after } = require('node:test') const { Readable } = require('node:stream') const { once } = require('node:events') +const FakeTimers = require('@sinonjs/fake-timers') /** * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore @@ -12,22 +13,27 @@ const { once } = require('node:events') */ function cacheStoreTests (CacheStore) { describe(CacheStore.prototype.constructor.name, () => { - test('matches interface', async () => { - const store = new CacheStore() - equal(typeof store.get, 'function') - equal(typeof store.createWriteStream, 'function') - equal(typeof store.delete, 'function') + test('matches interface', () => { + equal(typeof CacheStore.prototype.get, 'function') + equal(typeof CacheStore.prototype.createWriteStream, 'function') + equal(typeof CacheStore.prototype.delete, 'function') }) - // Checks that it can store & fetch different responses - test('basic functionality', async () => { - const request = { + test('caches request', async () => { + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey} + */ + const key = { origin: 'localhost', path: '/', method: 'GET', headers: {} } - const requestValue = { + + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue} + */ + const value = { statusCode: 200, statusMessage: '', headers: { foo: 'bar' }, @@ -36,40 +42,43 @@ function cacheStoreTests (CacheStore) { staleAt: Date.now() + 10000, deleteAt: Date.now() + 20000 } - const requestBody = ['asd', '123'] - /** - * @type {import('../../types/cache-interceptor.d.ts').default.CacheStore} - */ + const body = [Buffer.from('asd'), Buffer.from('123')] + const store = new CacheStore() // Sanity check - equal(store.get(request), undefined) + equal(await store.get(key), undefined) - // Write the response to the store - let writeStream = store.createWriteStream(request, requestValue) - notEqual(writeStream, undefined) - writeResponse(writeStream, requestBody) - - // Now try fetching it with a deep copy of the original request - let readResult = store.get(structuredClone(request)) - notEqual(readResult, undefined) + // Write response to store + { + const writable = store.createWriteStream(key, value) + notEqual(writable, undefined) + writeBody(writable, body) + } - deepStrictEqual(await readResponse(readResult), { - ...requestValue, - etag: undefined, - vary: undefined, - cacheControlDirectives: {}, - body: Buffer.concat(requestBody.map(x => Buffer.from(x))) - }) + // Now let's try fetching the response from the store + { + const result = await store.get(structuredClone(key)) + notEqual(result, undefined) + await compareGetResults(result, value, body) + } - // Now let's write another request to the store - const anotherRequest = { + /** + * Let's try out a request to a different resource to make sure it can + * differentiate between the two + * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey} + */ + const anotherKey = { origin: 'localhost', path: '/asd', method: 'GET', headers: {} } + + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue} + */ const anotherValue = { statusCode: 200, statusMessage: '', @@ -79,99 +88,87 @@ function cacheStoreTests (CacheStore) { staleAt: Date.now() + 10000, deleteAt: Date.now() + 20000 } - const anotherBody = ['asd2', '1234'] - // We haven't cached this one yet, make sure it doesn't confuse it with - // another request - equal(store.get(anotherRequest), undefined) + const anotherBody = [Buffer.from('asd'), Buffer.from('123')] - // Now let's cache it - writeStream = store.createWriteStream(anotherRequest, { - ...anotherValue, - body: [] - }) - notEqual(writeStream, undefined) - writeResponse(writeStream, anotherBody) - - readResult = store.get(anotherRequest) - notEqual(readResult, undefined) - deepStrictEqual(await readResponse(readResult), { - ...anotherValue, - etag: undefined, - vary: undefined, - cacheControlDirectives: {}, - body: Buffer.concat(anotherBody.map(x => Buffer.from(x))) - }) + equal(store.get(anotherKey), undefined) + + { + const writable = store.createWriteStream(anotherKey, anotherValue) + notEqual(writable, undefined) + writeBody(writable, anotherBody) + } + + { + const result = await store.get(structuredClone(anotherKey)) + notEqual(result, undefined) + await compareGetResults(result, anotherValue, anotherBody) + } }) - test('returns stale response if possible', async () => { - const request = { + test('returns stale response before deleteAt', async () => { + const clock = FakeTimers.install({ + shouldClearNativeTimers: true + }) + + after(() => clock.uninstall()) + + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey} + */ + const key = { origin: 'localhost', path: '/', method: 'GET', headers: {} } - const requestValue = { + + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue} + */ + const value = { statusCode: 200, statusMessage: '', headers: { foo: 'bar' }, cacheControlDirectives: {}, - cachedAt: Date.now() - 10000, - staleAt: Date.now() - 1, - deleteAt: Date.now() + 20000 + cachedAt: Date.now(), + staleAt: Date.now() + 1000, + // deleteAt is different because stale-while-revalidate, stale-if-error, ... + deleteAt: Date.now() + 5000 } - const requestBody = ['part1', 'part2'] - /** - * @type {import('../../types/cache-interceptor.d.ts').default.CacheStore} - */ - const store = new CacheStore() + const body = [Buffer.from('asd'), Buffer.from('123')] - const writeStream = store.createWriteStream(request, requestValue) - notEqual(writeStream, undefined) - writeResponse(writeStream, requestBody) + const store = new CacheStore() - const readResult = store.get(request) - deepStrictEqual(await readResponse(readResult), { - ...requestValue, - etag: undefined, - vary: undefined, - cacheControlDirectives: {}, - body: Buffer.concat(requestBody.map(x => Buffer.from(x))) - }) - }) + // Sanity check + equal(store.get(key), undefined) - test('doesn\'t return response past deletedAt', async () => { - const request = { - origin: 'localhost', - path: '/', - method: 'GET', - headers: {} - } - const requestValue = { - statusCode: 200, - statusMessage: '', - cachedAt: Date.now() - 20000, - headers: {}, - staleAt: Date.now() - 10000, - deleteAt: Date.now() - 5 + { + const writable = store.createWriteStream(key, value) + notEqual(writable, undefined) + writeBody(writable, body) } - const requestBody = ['part1', 'part2'] - /** - * @type {import('../../types/cache-interceptor.d.ts').default.CacheStore} - */ - const store = new CacheStore() + clock.tick(1500) + + { + const result = await store.get(structuredClone(key)) + notEqual(result, undefined) + await compareGetResults(result, value, body) + } - const writeStream = store.createWriteStream(request, requestValue) - notEqual(writeStream, undefined) - writeResponse(writeStream, requestBody) + clock.tick(6000) - equal(store.get(request), undefined) + // Past deleteAt, shouldn't be returned + equal(await store.get(key), undefined) }) - test('respects vary directives', async () => { - const request = { + test('vary directives used to decide which response to use', async () => { + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey} + */ + const key = { origin: 'localhost', path: '/', method: 'GET', @@ -179,7 +176,11 @@ function cacheStoreTests (CacheStore) { 'some-header': 'hello world' } } - const requestValue = { + + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue} + */ + const value = { statusCode: 200, statusMessage: '', headers: { foo: 'bar' }, @@ -188,54 +189,91 @@ function cacheStoreTests (CacheStore) { }, cacheControlDirectives: {}, cachedAt: Date.now(), - staleAt: Date.now() + 10000, - deleteAt: Date.now() + 20000 + staleAt: Date.now() + 1000, + deleteAt: Date.now() + 1000 } - const requestBody = ['part1', 'part2'] - /** - * @type {import('../../types/cache-interceptor.d.ts').default.CacheStore} - */ + const body = [Buffer.from('asd'), Buffer.from('123')] + const store = new CacheStore() // Sanity check - equal(store.get(request), undefined) - - const writeStream = store.createWriteStream(request, requestValue) - notEqual(writeStream, undefined) - writeResponse(writeStream, requestBody) - - const readStream = store.get(structuredClone(request)) - notEqual(readStream, undefined) - const { vary, ...responseValue } = requestValue - deepStrictEqual(await readResponse(readStream), { - ...responseValue, - etag: undefined, - vary: { 'some-header': 'hello world' }, - cacheControlDirectives: {}, - body: Buffer.concat(requestBody.map(x => Buffer.from(x))) - }) + equal(store.get(key), undefined) + + { + const writable = store.createWriteStream(key, value) + notEqual(writable, undefined) + writeBody(writable, body) + } - const nonMatchingRequest = { + { + const result = await store.get(structuredClone(key)) + notEqual(result, undefined) + await compareGetResults(result, value, body) + } + + /** + * Let's make another key to the same resource but with a different vary + * header + * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey} + */ + const anotherKey = { origin: 'localhost', path: '/', method: 'GET', headers: { - 'some-header': 'another-value' + 'some-header': 'hello world2' } } - equal(store.get(nonMatchingRequest), undefined) + + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue} + */ + const anotherValue = { + statusCode: 200, + statusMessage: '', + headers: { foo: 'bar' }, + vary: { + 'some-header': 'hello world2' + }, + cacheControlDirectives: {}, + cachedAt: Date.now(), + staleAt: Date.now() + 1000, + deleteAt: Date.now() + 1000 + } + + const anotherBody = [Buffer.from('asd'), Buffer.from('123')] + + equal(await store.get(anotherKey), undefined) + + { + const writable = store.createWriteStream(anotherKey, anotherValue) + notEqual(writable, undefined) + writeBody(writable, anotherBody) + } + + { + const result = await store.get(structuredClone(key)) + notEqual(result, undefined) + await compareGetResults(result, value, body) + } + + { + const result = await store.get(structuredClone(anotherKey)) + notEqual(result, undefined) + await compareGetResults(result, anotherValue, anotherBody) + } }) }) } /** * @param {import('node:stream').Writable} stream - * @param {string[]} body + * @param {Buffer[]} body */ -function writeResponse (stream, body) { +function writeBody (stream, body) { for (const chunk of body) { - stream.write(Buffer.from(chunk)) + stream.write(chunk) } stream.end() @@ -243,33 +281,74 @@ function writeResponse (stream, body) { } /** - * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result - * @returns {Promise} + * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} param0 + * @returns {Promise} */ -async function readResponse ({ body: src, ...response }) { - notEqual(response, undefined) - notEqual(src, undefined) +async function readBody ({ body }) { + if (!body) { + return undefined + } - const stream = Readable.from(src ?? []) + if (typeof body === 'string') { + return [Buffer.from(body)] + } + + if (body.constructor.name === 'Buffer') { + return [body] + } + + const stream = Readable.from(body) /** * @type {Buffer[]} */ - const body = [] + const streamedBody = [] + stream.on('data', chunk => { - body.push(Buffer.from(chunk)) + streamedBody.push(Buffer.from(chunk)) }) await once(stream, 'end') - return { - ...response, - body: Buffer.concat(body) + return streamedBody +} + +/** + * @param {Buffer[]} buffers + * @returns {Buffer} + */ +function joinBufferArray (buffers) { + const data = [] + + for (const buffer of buffers) { + buffer.forEach((chunk) => { + data.push(chunk) + }) + } + + return Buffer.from(data) +} + +/** + * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} actual + * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} expected + * @param {Buffer[]} expectedBody +*/ +async function compareGetResults (actual, expected, expectedBody) { + const actualBody = await readBody(actual) + deepStrictEqual( + actualBody ? joinBufferArray(actualBody) : undefined, + joinBufferArray(expectedBody) + ) + + for (const key of Object.keys(expected)) { + deepStrictEqual(actual[key], expected[key]) } } module.exports = { cacheStoreTests, - writeResponse, - readResponse + writeBody, + readBody, + compareGetResults } diff --git a/test/cache-interceptor/sqlite-cache-store-tests.js b/test/cache-interceptor/sqlite-cache-store-tests.js index 7a129dd61fc..34e4fd6257d 100644 --- a/test/cache-interceptor/sqlite-cache-store-tests.js +++ b/test/cache-interceptor/sqlite-cache-store-tests.js @@ -1,10 +1,9 @@ 'use strict' const { test, skip } = require('node:test') -const { deepStrictEqual, notEqual, strictEqual } = require('node:assert') +const { notEqual, strictEqual } = require('node:assert') const { rm } = require('node:fs/promises') -const { cacheStoreTests, writeResponse, readResponse } = require('./cache-store-test-utils.js') -const { once } = require('node:events') +const { cacheStoreTests, writeBody, compareGetResults } = require('./cache-store-test-utils.js') let hasSqlite = false try { @@ -42,14 +41,20 @@ test('SqliteCacheStore works nicely with multiple stores', async (t) => { await rm(sqliteLocation) }) - const request = { + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey} + */ + const key = { origin: 'localhost', path: '/', method: 'GET', headers: {} } - const requestValue = { + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue} + */ + const value = { statusCode: 200, statusMessage: '', headers: { foo: 'bar' }, @@ -57,33 +62,28 @@ test('SqliteCacheStore works nicely with multiple stores', async (t) => { staleAt: Date.now() + 10000, deleteAt: Date.now() + 20000 } - const requestBody = ['asd', '123'] - const writable = storeA.createWriteStream(request, requestValue) - notEqual(writable, undefined) - writeResponse(writable, requestBody) + const body = [Buffer.from('asd'), Buffer.from('123')] + + { + const writable = storeA.createWriteStream(key, value) + notEqual(writable, undefined) + writeBody(writable, body) + } // Make sure we got the expected response from store a - let readable = storeA.get(request) - notEqual(readable, undefined) - deepStrictEqual(await readResponse(readable), { - ...requestValue, - etag: undefined, - vary: undefined, - cacheControlDirectives: undefined, - body: Buffer.concat(requestBody.map(x => Buffer.from(x))) - }) + { + const result = storeA.get(structuredClone(key)) + notEqual(result, undefined) + await compareGetResults(result, value, body) + } // Make sure we got the expected response from store b - readable = storeB.get(request) - notEqual(readable, undefined) - deepStrictEqual(await readResponse(readable), { - ...requestValue, - etag: undefined, - vary: undefined, - cacheControlDirectives: undefined, - body: Buffer.concat(requestBody.map(x => Buffer.from(x))) - }) + { + const result = storeB.get(structuredClone(key)) + notEqual(result, undefined) + await compareGetResults(result, value, body) + } }) test('SqliteCacheStore maxEntries', async (t) => { @@ -93,26 +93,26 @@ test('SqliteCacheStore maxEntries', async (t) => { } const SqliteCacheStore = require('../../lib/cache/sqlite-cache-store.js') - const sqliteLocation = 'cache-interceptor.sqlite' const store = new SqliteCacheStore({ - location: sqliteLocation, maxCount: 10 }) - t.after(async () => { - await rm(sqliteLocation) - }) - for (let i = 0; i < 20; i++) { - const request = { + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey} + */ + const key = { origin: 'localhost', path: '/' + i, method: 'GET', headers: {} } - const requestValue = { + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue} + */ + const value = { statusCode: 200, statusMessage: '', headers: { foo: 'bar' }, @@ -120,42 +120,43 @@ test('SqliteCacheStore maxEntries', async (t) => { staleAt: Date.now() + 10000, deleteAt: Date.now() + 20000 } - const requestBody = ['asd', '123'] - const writable = store.createWriteStream(request, requestValue) + const body = ['asd', '123'] + + const writable = store.createWriteStream(key, value) notEqual(writable, undefined) - await once(writeResponse(writable, requestBody), 'close') + writeBody(writable, body) } strictEqual(store.size <= 11, true) }) -test('two writes', async (t) => { +test('SqliteCacheStore two writes', async (t) => { if (!hasSqlite) { t.skip() return } const SqliteCacheStore = require('../../lib/cache/sqlite-cache-store.js') - const sqliteLocation = 'cache-interceptor.sqlite' const store = new SqliteCacheStore({ - location: sqliteLocation, maxCount: 10 }) - t.after(async () => { - await rm(sqliteLocation) - }) - - const request = { + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey} + */ + const key = { origin: 'localhost', path: '/', method: 'GET', headers: {} } - const requestValue = { + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue} + */ + const value = { statusCode: 200, statusMessage: '', headers: { foo: 'bar' }, @@ -163,15 +164,18 @@ test('two writes', async (t) => { staleAt: Date.now() + 10000, deleteAt: Date.now() + 20000 } - const requestBody = ['asd', '123'] + + const body = ['asd', '123'] { - const writable = store.createWriteStream(request, requestValue) - await once(writeResponse(writable, requestBody), 'close') + const writable = store.createWriteStream(key, value) + notEqual(writable, undefined) + writeBody(writable, body) } { - const writable = store.createWriteStream(request, requestValue) - await once(writeResponse(writable, requestBody), 'close') + const writable = store.createWriteStream(key, value) + notEqual(writable, undefined) + writeBody(writable, body) } }) diff --git a/test/interceptors/cache.js b/test/interceptors/cache.js index ea6ddf9c111..1b502c212b3 100644 --- a/test/interceptors/cache.js +++ b/test/interceptors/cache.js @@ -1,18 +1,18 @@ 'use strict' -const { describe, test, after } = require('node:test') -const { strictEqual, notEqual, fail, equal } = require('node:assert') const { createServer } = require('node:http') +const { describe, test, after } = require('node:test') const { once } = require('node:events') +const { equal, strictEqual, notEqual, fail } = require('node:assert') const FakeTimers = require('@sinonjs/fake-timers') -const { Client, interceptors, cacheStores } = require('../../index') +const { Client, interceptors, cacheStores: { MemoryCacheStore } } = require('../../index') describe('Cache Interceptor', () => { - test('doesn\'t cache request w/ no cache-control header', async () => { + test('caches request', async () => { let requestsToOrigin = 0 - const server = createServer((_, res) => { requestsToOrigin++ + res.setHeader('cache-control', 's-maxage=10') res.end('asd') }).listen(0) @@ -26,77 +26,39 @@ describe('Cache Interceptor', () => { await once(server, 'listening') - strictEqual(requestsToOrigin, 0) - - // Send initial request. This should reach the origin - let response = await client.request({ - origin: 'localhost', - method: 'GET', - path: '/' - }) - strictEqual(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') + // Sanity check + equal(requestsToOrigin, 0) - // Send second request that should be handled by cache - response = await client.request({ + /** + * @type {import('../../types/dispatcher').default.RequestOptions} + */ + const request = { origin: 'localhost', method: 'GET', path: '/' - }) - strictEqual(requestsToOrigin, 2) - strictEqual(await response.body.text(), 'asd') - }) - - test('caches request successfully', async () => { - let requestsToOrigin = 0 - - const server = createServer((_, res) => { - requestsToOrigin++ - res.setHeader('cache-control', 'public, s-maxage=10') - res.end('asd') - }).listen(0) - - const client = new Client(`http://localhost:${server.address().port}`) - .compose(interceptors.cache()) - - after(async () => { - server.close() - await client.close() - }) - - await once(server, 'listening') - - strictEqual(requestsToOrigin, 0) + } - // Send initial request. This should reach the origin - let response = await client.request({ - origin: 'localhost', - method: 'GET', - path: '/' - }) - strictEqual(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') + { + const res = await client.request(request) + equal(requestsToOrigin, 1) + strictEqual(await res.body.text(), 'asd') + } - // Send second request that should be handled by cache - response = await client.request({ - origin: 'localhost', - method: 'GET', - path: '/' - }) - strictEqual(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') - strictEqual(response.headers.age, '0') + { + const res = await client.request(request) + equal(requestsToOrigin, 1) + strictEqual(await res.body.text(), 'asd') + } }) - test('respects vary header', async () => { + test('vary directives used to decide which response to use', async () => { let requestsToOrigin = 0 - const server = createServer((req, res) => { requestsToOrigin++ - res.setHeader('cache-control', 'public, s-maxage=10') - res.setHeader('vary', 'some-header, another-header') + res.setHeader('cache-control', 's-maxage=10') + res.setHeader('vary', 'a') - if (req.headers['some-header'] === 'abc123') { + if (req.headers.a === 'asd123') { res.end('asd') } else { res.end('dsa') @@ -113,48 +75,60 @@ describe('Cache Interceptor', () => { await once(server, 'listening') - strictEqual(requestsToOrigin, 0) - - // Send initial request. This should reach the origin - let response = await client.request({ + /** + * @type {import('../../types/dispatcher').default.RequestOptions} + */ + const requestA = { origin: 'localhost', method: 'GET', path: '/', headers: { - 'some-header': 'abc123', - 'another-header': '123abc' + a: 'asd123' } - }) - strictEqual(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') - - // Make another request with changed headers, this should miss - const secondResponse = await client.request({ - method: 'GET', - path: '/', - headers: { - 'some-header': 'qwerty', - 'another-header': 'asdfg' - } - }) - strictEqual(requestsToOrigin, 2) - strictEqual(await secondResponse.body.text(), 'dsa') + } - // Resend the first request again which should still be cahced - response = await client.request({ + /** + * @type {import('../../types/dispatcher').default.RequestOptions} + */ + const requestB = { origin: 'localhost', method: 'GET', path: '/', headers: { - 'some-header': 'abc123', - 'another-header': '123abc' + a: 'dsa' } - }) - strictEqual(requestsToOrigin, 2) - strictEqual(await response.body.text(), 'asd') + } + + // Should reach origin + { + const res = await client.request(requestA) + equal(requestsToOrigin, 1) + strictEqual(await res.body.text(), 'asd') + } + + // Should reach origin + { + const res = await client.request(requestB) + equal(requestsToOrigin, 2) + strictEqual(await res.body.text(), 'dsa') + } + + // Should be cached + { + const res = await client.request(requestA) + equal(requestsToOrigin, 2) + strictEqual(await res.body.text(), 'asd') + } + + // Should be cached + { + const res = await client.request(requestB) + equal(requestsToOrigin, 2) + strictEqual(await res.body.text(), 'dsa') + } }) - test('vary headers are present in revalidation request', async () => { + test('stale responses are revalidated before deleteAt (if-modified-since)', async () => { const clock = FakeTimers.install({ shouldClearNativeTimers: true }) @@ -165,19 +139,18 @@ describe('Cache Interceptor', () => { res.setHeader('date', 0) res.setHeader('cache-control', 's-maxage=1, stale-while-revalidate=10') - if (requestsToOrigin === 0) { - requestsToOrigin++ - res.setHeader('vary', 'a, b') - res.setHeader('etag', '"asd"') - res.end('asd') - } else { + if (req.headers['if-modified-since']) { revalidationRequests++ - notEqual(req.headers['if-none-match'], undefined) - notEqual(req.headers['a'], undefined) - notEqual(req.headers['b'], undefined) - res.statusCode = 304 - res.end() + if (revalidationRequests === 2) { + res.end('updated') + } else { + res.statusCode = 304 + res.end() + } + } else { + requestsToOrigin++ + res.end('asd') } }).listen(0) @@ -195,56 +168,74 @@ describe('Cache Interceptor', () => { strictEqual(requestsToOrigin, 0) strictEqual(revalidationRequests, 0) + /** + * @type {import('../../types/dispatcher').default.RequestOptions} + */ const request = { origin: 'localhost', - path: '/', method: 'GET', - headers: { - a: 'asd', - b: 'asd' - } + path: '/' } + // Send initial request. This should reach the origin { - const response = await client.request(request) - strictEqual(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') + const res = await client.request(request) + equal(requestsToOrigin, 1) + equal(revalidationRequests, 0) + strictEqual(await res.body.text(), 'asd') } clock.tick(1500) + // Response is now stale, the origin should get a revalidation request { - const response = await client.request(request) - strictEqual(requestsToOrigin, 1) - strictEqual(revalidationRequests, 1) - strictEqual(await response.body.text(), 'asd') + const res = await client.request(request) + equal(requestsToOrigin, 1) + equal(revalidationRequests, 1) + strictEqual(await res.body.text(), 'asd') } - }) - test('revalidates request when needed', async () => { - let requestsToOrigin = 0 + // Response is still stale, but revalidation should fail now. + { + const res = await client.request(request) + equal(requestsToOrigin, 1) + equal(revalidationRequests, 2) + strictEqual(await res.body.text(), 'updated') + } + }) + test('stale responses are revalidated before deleteAt (if-none-match)', async () => { const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + let requestsToOrigin = 0 + let revalidationRequests = 0 + let serverError const server = createServer((req, res) => { res.setHeader('date', 0) - res.setHeader('cache-control', 'public, s-maxage=1, stale-while-revalidate=10') + res.setHeader('cache-control', 's-maxage=1, stale-while-revalidate=10') - requestsToOrigin++ + try { + if (req.headers['if-none-match']) { + revalidationRequests++ - if (requestsToOrigin > 1) { - notEqual(req.headers['if-modified-since'], undefined) + equal(req.headers['if-none-match'], '"asd123"') - if (requestsToOrigin === 3) { - res.end('asd123') + if (revalidationRequests === 2) { + res.end('updated') + } else { + res.statusCode = 304 + res.end() + } } else { - res.statusCode = 304 - res.end() + requestsToOrigin++ + res.setHeader('etag', '"asd123"') + res.end('asd') } - } else { - res.end('asd') + } catch (err) { + serverError = err + res.end() } }).listen(0) @@ -260,7 +251,11 @@ describe('Cache Interceptor', () => { await once(server, 'listening') strictEqual(requestsToOrigin, 0) + strictEqual(revalidationRequests, 0) + /** + * @type {import('../../types/dispatcher').default.RequestOptions} + */ const request = { origin: 'localhost', method: 'GET', @@ -268,50 +263,75 @@ describe('Cache Interceptor', () => { } // Send initial request. This should reach the origin - let response = await client.request(request) - strictEqual(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') + { + const res = await client.request(request) + if (serverError) { + throw serverError + } + + equal(requestsToOrigin, 1) + equal(revalidationRequests, 0) + strictEqual(await res.body.text(), 'asd') + } clock.tick(1500) - // Now we send two more requests. Both of these should reach the origin, - // but now with a conditional header asking if the resource has been - // updated. These need to be ran after the response is stale. - // No update for the second request - response = await client.request(request) - strictEqual(requestsToOrigin, 2) - strictEqual(await response.body.text(), 'asd') - - // This should be updated, even though the value isn't expired. - response = await client.request(request) - strictEqual(requestsToOrigin, 3) - strictEqual(await response.body.text(), 'asd123') - }) + // Response is now stale, the origin should get a revalidation request + { + const res = await client.request(request) + if (serverError) { + throw serverError + } - test('revalidates request w/ etag when provided', async (t) => { - let requestsToOrigin = 0 + equal(requestsToOrigin, 1) + equal(revalidationRequests, 1) + strictEqual(await res.body.text(), 'asd') + } + + // Response is still stale, but revalidation should fail now. + { + const res = await client.request(request) + if (serverError) { + throw serverError + } + + equal(requestsToOrigin, 1) + equal(revalidationRequests, 2) + strictEqual(await res.body.text(), 'updated') + } + }) + test('vary headers are present in revalidation request', async () => { const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + let requestsToOrigin = 0 + let revalidationRequests = 0 + let serverError const server = createServer((req, res) => { res.setHeader('date', 0) - res.setHeader('cache-control', 'public, s-maxage=1, stale-while-revalidate=10') - requestsToOrigin++ + res.setHeader('cache-control', 's-maxage=1, stale-while-revalidate=10') - if (requestsToOrigin > 1) { - equal(req.headers['if-none-match'], '"asd123"') + try { + const ifNoneMatch = req.headers['if-none-match'] + + if (ifNoneMatch) { + revalidationRequests++ + notEqual(req.headers.a, undefined) + notEqual(req.headers.b, undefined) - if (requestsToOrigin === 3) { - res.end('asd123') - } else { res.statusCode = 304 res.end() + } else { + requestsToOrigin++ + res.setHeader('vary', 'a, b') + res.setHeader('etag', '"asd"') + res.end('asd') } - } else { - res.setHeader('etag', '"asd123"') - res.end('asd') + } catch (err) { + serverError = err + res.end() } }).listen(0) @@ -327,80 +347,55 @@ describe('Cache Interceptor', () => { await once(server, 'listening') strictEqual(requestsToOrigin, 0) + strictEqual(revalidationRequests, 0) const request = { origin: 'localhost', + path: '/', method: 'GET', - path: '/' + headers: { + a: 'asd', + b: 'asd' + } } - // Send initial request. This should reach the origin - let response = await client.request(request) - strictEqual(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') - - clock.tick(1500) - - // Now we send two more requests. Both of these should reach the origin, - // but now with a conditional header asking if the resource has been - // updated. These need to be ran after the response is stale. - // No update for the second request - response = await client.request(request) - strictEqual(requestsToOrigin, 2) - strictEqual(await response.body.text(), 'asd') - - // This should be updated, even though the value isn't expired. - response = await client.request(request) - strictEqual(requestsToOrigin, 3) - strictEqual(await response.body.text(), 'asd123') - }) - - test('respects cache store\'s isFull property', async () => { - const server = createServer((_, res) => { - res.end('asd') - }).listen(0) - - after(() => server.close()) - await once(server, 'listening') - - const store = new cacheStores.MemoryCacheStore() - Object.defineProperty(store, 'isFull', { - value: true - }) + { + const response = await client.request(request) + if (serverError) { + throw serverError + } - store.createWriteStream = (...args) => { - fail('shouln\'t have reached this') + strictEqual(requestsToOrigin, 1) + strictEqual(await response.body.text(), 'asd') } - const client = new Client(`http://localhost:${server.address().port}`) - .compose(interceptors.cache({ store })) + clock.tick(1500) - await client.request({ - origin: 'localhost', - method: 'GET', - path: '/', - headers: { - 'some-header': 'abc123', - 'another-header': '123abc' + { + const response = await client.request(request) + if (serverError) { + throw serverError } - }) + + strictEqual(requestsToOrigin, 1) + strictEqual(revalidationRequests, 1) + strictEqual(await response.body.text(), 'asd') + } }) - test('unsafe methods call the store\'s delete function', async () => { - const server = createServer((_, res) => { - res.end('asd') - }).listen(0) + test('unsafe methods cause resource to be purged from cache', async () => { + const server = createServer((_, res) => res.end('asd')).listen(0) after(() => server.close()) await once(server, 'listening') - let deleteCalled = false - const store = new cacheStores.MemoryCacheStore() + const store = new MemoryCacheStore() - const originaldelete = store.delete.bind(store) + let deleteCalled = false + const originalDelete = store.delete.bind(store) store.delete = (key) => { deleteCalled = true - originaldelete(key) + originalDelete(key) } const client = new Client(`http://localhost:${server.address().port}`) @@ -409,143 +404,38 @@ describe('Cache Interceptor', () => { methods: ['GET'] // explicitly only cache GET methods })) - // Make sure safe methods that we want to cache don't cause a cache purge - await client.request({ + /** + * @type {import('../../types/dispatcher').default.RequestOptions} + */ + const request = { origin: 'localhost', method: 'GET', path: '/' - }) - - equal(deleteCalled, false) + } - // Make sure other safe methods that we don't want to cache don't cause a cache purge - await client.request({ - origin: 'localhost', - method: 'HEAD', - path: '/' - }) + // Send initial request, will cache the response + await client.request(request) - strictEqual(deleteCalled, false) + // Sanity check + equal(deleteCalled, false) // Make sure the common unsafe methods cause cache purges for (const method of ['POST', 'PUT', 'PATCH', 'DELETE']) { deleteCalled = false await client.request({ - origin: 'localhost', - method, - path: '/' + ...request, + method }) equal(deleteCalled, true, method) } }) - test('necessary headers are stripped', async () => { - let requestsToOrigin = 0 - const server = createServer((req, res) => { - requestsToOrigin++ - res.setHeader('cache-control', 'public, s-maxage=10, no-cache=should-be-stripped') - res.setHeader('should-be-stripped', 'hello world') - res.setHeader('should-not-be-stripped', 'dsa321') - - res.end('asd') - }).listen(0) - - const client = new Client(`http://localhost:${server.address().port}`) - .compose(interceptors.cache()) - - after(async () => { - server.close() - await client.close() - }) - - await once(server, 'listening') - - const request = { - origin: 'localhost', - method: 'GET', - path: '/' - } - - // Send initial request. This should reach the origin - { - const response = await client.request(request) - equal(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') - equal(response.headers['should-be-stripped'], 'hello world') - equal(response.headers['should-not-be-stripped'], 'dsa321') - } - - // Send second request, this should hit the cache - { - const response = await client.request(request) - equal(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') - equal(response.headers['should-be-stripped'], undefined) - equal(response.headers['should-not-be-stripped'], 'dsa321') - } - }) - - test('necessary headers are stripped (quotes)', async () => { - let requestsToOrigin = 0 + test('unsafe methods aren\'t cached', async () => { const server = createServer((_, res) => { - requestsToOrigin++ - res.setHeader('connection', 'a, b') - res.setHeader('a', '123') - res.setHeader('b', '123') - res.setHeader('cache-control', 's-maxage=3600, no-cache="should-be-stripped, should-be-stripped2"') - res.setHeader('should-be-stripped', 'hello world') - res.setHeader('should-be-stripped2', 'hello world') - res.setHeader('should-not-be-stripped', 'dsa321') - - res.end('asd') - }).listen(0) - - const client = new Client(`http://localhost:${server.address().port}`) - .compose(interceptors.cache()) - - after(async () => { - server.close() - await client.close() - }) - - await once(server, 'listening') - - const request = { - origin: 'localhost', - method: 'GET', - path: '/' - } - - // Send initial request. This should reach the origin - { - const response = await client.request(request) - equal(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') - equal(response.headers['a'], '123') - equal(response.headers['b'], '123') - equal(response.headers['should-be-stripped'], 'hello world') - equal(response.headers['should-be-stripped2'], 'hello world') - equal(response.headers['should-not-be-stripped'], 'dsa321') - } - - // Send second request, this should hit the cache - { - const response = await client.request(request) - equal(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') - equal(response.headers['a'], undefined) - equal(response.headers['b'], undefined) - equal(response.headers['should-be-stripped'], undefined) - equal(response.headers['should-be-stripped2'], undefined) - } - }) - - test('requests w/ unsafe methods never get cached', async () => { - const server = createServer((req, res) => { res.setHeader('cache-control', 'public, s-maxage=1') - res.end('asd') + res.end('') }).listen(0) after(() => server.close()) @@ -572,106 +462,114 @@ describe('Cache Interceptor', () => { } }) - for (const maxAgeHeader of ['s-maxage', 'max-age']) { - test(`stale-while-revalidate w/ ${maxAgeHeader}`, async () => { - const clock = FakeTimers.install({ - shouldClearNativeTimers: true - }) - - let requestsToOrigin = 0 - let revalidationRequests = 0 - const server = createServer((req, res) => { - res.setHeader('date', 0) - - if (req.headers['if-none-match']) { - revalidationRequests++ - if (req.headers['if-none-match'] !== '"asd"') { - fail(`etag mismatch: ${req.headers['if-none-match']}`) - } - - res.statusCode = 304 - res.end() - } else { - requestsToOrigin++ - res.setHeader('cache-control', 'public, max-age=1, stale-while-revalidate=4') - res.setHeader('etag', '"asd"') - res.end('asd') - } - }).listen(0) - - const client = new Client(`http://localhost:${server.address().port}`) - .compose(interceptors.cache()) + test('necessary headers are stripped', async () => { + const headers = [ + // Headers defined in the spec that we need to strip + 'connection', + 'proxy-authenticate', + 'proxy-authentication-info', + 'proxy-authorization', + 'proxy-connection', + 'te', + 'upgrade', + // Headers we need to specifiy to be stripped + 'should-be-stripped' + ] + + let requestToOrigin = 0 + const server = createServer((_, res) => { + requestToOrigin++ + res.setHeader('cache-control', 's-maxage=10, no-cache=should-be-stripped') + res.setHeader('should-not-be-stripped', 'asd') - after(async () => { - clock.uninstall() - server.close() - await client.close() - }) + for (const header of headers) { + res.setHeader(header, 'asd') + } - await once(server, 'listening') + res.end() + }).listen(0) - strictEqual(requestsToOrigin, 0) - strictEqual(revalidationRequests, 0) + const client = new Client(`http://localhost:${server.address().port}`) + .compose(interceptors.cache()) - // Send first request, this will hit the origin - { - const response = await client.request({ - origin: 'localhost', - path: '/', - method: 'GET' - }) - equal(requestsToOrigin, 1) - strictEqual(revalidationRequests, 0) - equal(response.statusCode, 200) - equal(await response.body.text(), 'asd') - } + after(async () => { + server.close() + await client.close() + }) - // Send second request, this will be cached. - { - const response = await client.request({ - origin: 'localhost', - path: '/', - method: 'GET' - }) - equal(requestsToOrigin, 1) - strictEqual(revalidationRequests, 0) - equal(response.statusCode, 200) - equal(await response.body.text(), 'asd') - } + await once(server, 'listening') - clock.tick(1500) + /** + * @type {import('../../types/dispatcher').default.RequestOptions} + */ + const request = { + origin: 'localhost', + method: 'GET', + path: '/' + } - // Send third request, this should be revalidated - { - const response = await client.request({ - origin: 'localhost', - path: '/', - method: 'GET' - }) - equal(requestsToOrigin, 1) - strictEqual(revalidationRequests, 1) - equal(response.statusCode, 200) - equal(await response.body.text(), 'asd') + { + const res = await client.request(request) + equal(requestToOrigin, 1) + equal(res.headers['should-not-be-stripped'], 'asd') + + for (const header of headers) { + equal(res.headers[header], 'asd') } + } - clock.tick(5000) + { + const res = await client.request(request) + equal(requestToOrigin, 1) + equal(res.headers['should-not-be-stripped'], 'asd') + equal(res.headers['transfer-encoding'], undefined) - // Send fourth request, this should be a new request entirely - { - const response = await client.request({ - origin: 'localhost', - path: '/', - method: 'GET' - }) - equal(requestsToOrigin, 2) - strictEqual(revalidationRequests, 1) - equal(response.statusCode, 200) - equal(await response.body.text(), 'asd') + for (const header of headers) { + equal(res.headers[header], undefined) } - }) - } + } + }) + + test('cacheByDefault', async () => { + let requestsToOrigin = 0 + const server = createServer((_, res) => { + requestsToOrigin++ + res.end('asd') + }).listen(0) + + after(() => server.close()) + + const client = new Client(`http://localhost:${server.address().port}`) + .compose(interceptors.cache({ + cacheByDefault: 3600 + })) + + equal(requestsToOrigin, 0) + + // Should hit the origin + { + const res = await client.request({ + origin: 'localhost', + path: '/', + method: 'GET' + }) + equal(requestsToOrigin, 1) + equal(await res.body.text(), 'asd') + } + + // Should hit the cache + { + const res = await client.request({ + origin: 'localhost', + path: '/', + method: 'GET' + }) + equal(requestsToOrigin, 1) + equal(await res.body.text(), 'asd') + } + }) - test('stale-if-error from response works as expected', async () => { + test('stale-if-error (response)', async () => { const clock = FakeTimers.install({ shouldClearNativeTimers: true }) @@ -704,13 +602,18 @@ describe('Cache Interceptor', () => { strictEqual(requestsToOrigin, 0) + /** + * @type {import('../../types/dispatcher').default.RequestOptions} + */ + const request = { + origin: 'localhost', + method: 'GET', + path: '/' + } + // Send first request. This will hit the origin and succeed { - const response = await client.request({ - origin: 'localhost', - path: '/', - method: 'GET' - }) + const response = await client.request(request) equal(requestsToOrigin, 1) equal(response.statusCode, 200) equal(await response.body.text(), 'asd') @@ -719,11 +622,7 @@ describe('Cache Interceptor', () => { // Send second request. It isn't stale yet, so this should be from the // cache and succeed { - const response = await client.request({ - origin: 'localhost', - path: '/', - method: 'GET' - }) + const response = await client.request(request) equal(requestsToOrigin, 1) equal(response.statusCode, 200) equal(await response.body.text(), 'asd') @@ -734,11 +633,7 @@ describe('Cache Interceptor', () => { // Send third request. This is now stale, the revalidation request should // fail but the response should still be served from cache. { - const response = await client.request({ - origin: 'localhost', - path: '/', - method: 'GET' - }) + const response = await client.request(request) equal(requestsToOrigin, 2) equal(response.statusCode, 200) equal(await response.body.text(), 'asd') @@ -749,55 +644,12 @@ describe('Cache Interceptor', () => { // Send fourth request. We're now outside the stale-if-error threshold and // should see the error. { - const response = await client.request({ - origin: 'localhost', - path: '/', - method: 'GET' - }) + const response = await client.request(request) equal(requestsToOrigin, 3) equal(response.statusCode, 500) } }) - test('cacheByDefault', async () => { - let requestsToOrigin = 0 - const server = createServer((_, res) => { - requestsToOrigin++ - res.end('asd') - }).listen(0) - - after(() => server.close()) - - const client = new Client(`http://localhost:${server.address().port}`) - .compose(interceptors.cache({ - cacheByDefault: 3600 - })) - - equal(requestsToOrigin, 0) - - // Should hit the origin - { - const res = await client.request({ - origin: 'localhost', - path: '/', - method: 'GET' - }) - equal(requestsToOrigin, 1) - equal(await res.body.text(), 'asd') - } - - // Should hit the cache - { - const res = await client.request({ - origin: 'localhost', - path: '/', - method: 'GET' - }) - equal(requestsToOrigin, 1) - equal(await res.body.text(), 'asd') - } - }) - describe('Client-side directives', () => { test('max-age', async () => { const clock = FakeTimers.install({ @@ -807,8 +659,9 @@ describe('Cache Interceptor', () => { let requestsToOrigin = 0 const server = createServer((_, res) => { requestsToOrigin++ + res.setHeader('date', 0) res.setHeader('cache-control', 'public, s-maxage=100') - res.end('asd') + res.end() }).listen(0) const client = new Client(`http://localhost:${server.address().port}`) @@ -824,74 +677,60 @@ describe('Cache Interceptor', () => { strictEqual(requestsToOrigin, 0) - // Send initial request. This should reach the origin - let response = await client.request({ + /** + * @type {import('../../types/dispatcher').default.RequestOptions} + */ + const request = { origin: 'localhost', method: 'GET', path: '/' - }) - strictEqual(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') + } - // Send second request that should be handled by cache - response = await client.request({ - origin: 'localhost', - method: 'GET', - path: '/' - }) - strictEqual(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') - strictEqual(response.headers.age, '0') + // Send first request to cache the response + await client.request(request) + equal(requestsToOrigin, 1) - // Send third request w/ the directive, this should be handled by the cache - response = await client.request({ - origin: 'localhost', - method: 'GET', - path: '/', + // Send second request, should be served by the cache since it's within + // the window + await client.request({ + ...request, headers: { 'cache-control': 'max-age=5' } }) - strictEqual(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') + equal(requestsToOrigin, 1) clock.tick(6000) - // Send fourth request w/ the directive, age should be 6 now so this - // should hit the origin - response = await client.request({ - origin: 'localhost', - method: 'GET', - path: '/', + // Send third request, should reach the origin + await client.request({ + ...request, headers: { 'cache-control': 'max-age=5' } }) - strictEqual(requestsToOrigin, 2) - strictEqual(await response.body.text(), 'asd') + equal(requestsToOrigin, 2) }) test('max-stale', async () => { - let requestsToOrigin = 0 - const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + let requestsToOrigin = 0 + let revalidationRequests = 0 const server = createServer((req, res) => { res.setHeader('date', 0) res.setHeader('cache-control', 'public, s-maxage=1, stale-while-revalidate=10') - if (requestsToOrigin === 1) { - notEqual(req.headers['if-modified-since'], undefined) - + if (req.headers['if-modified-since']) { + revalidationRequests++ res.statusCode = 304 res.end() } else { + requestsToOrigin++ res.end('asd') } - - requestsToOrigin++ }).listen(0) const client = new Client(`http://localhost:${server.address().port}`) @@ -907,54 +746,53 @@ describe('Cache Interceptor', () => { strictEqual(requestsToOrigin, 0) + /** + * @type {import('../../types/dispatcher').default.RequestOptions} + */ const request = { origin: 'localhost', method: 'GET', path: '/' } - // Send initial request. This should reach the origin - let response = await client.request(request) - strictEqual(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') + await client.request(request) + equal(requestsToOrigin, 1) + equal(revalidationRequests, 0) clock.tick(1500) - // Now we send a second request. This should be within the max stale - // threshold, so a request shouldn't be made to the origin - response = await client.request({ + // Send second request within the max-stale threshold + await client.request({ ...request, headers: { 'cache-control': 'max-stale=5' } }) - strictEqual(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') + equal(requestsToOrigin, 1) + equal(revalidationRequests, 0) - // Send a third request. This shouldn't be within the max stale threshold - // so a request should be made to the origin - response = await client.request({ + // Send third request outside the max-stale threshold + await client.request({ ...request, headers: { 'cache-control': 'max-stale=0' } }) - strictEqual(requestsToOrigin, 2) - strictEqual(await response.body.text(), 'asd') + equal(requestsToOrigin, 1) + equal(revalidationRequests, 1) }) test('min-fresh', async () => { - let requestsToOrigin = 0 - const clock = FakeTimers.install({ shouldClearNativeTimers: true }) - const server = createServer((req, res) => { + let requestsToOrigin = 0 + const server = createServer((_, res) => { requestsToOrigin++ res.setHeader('date', 0) res.setHeader('cache-control', 'public, s-maxage=10') - res.end('asd') + res.end() }).listen(0) const client = new Client(`http://localhost:${server.address().port}`) @@ -970,59 +808,55 @@ describe('Cache Interceptor', () => { strictEqual(requestsToOrigin, 0) + /** + * @type {import('../../types/dispatcher').default.RequestOptions} + */ const request = { origin: 'localhost', method: 'GET', path: '/' } - // Send initial request. This should reach the origin - let response = await client.request(request) - strictEqual(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') + await client.request(request) + equal(requestsToOrigin, 1) - // Fast forward more. Response has 8sec TTL left after + // Fast forward to response having 8sec ttl clock.tick(2000) - // Now we send a second request. This should be within the threshold, so - // a request shouldn't be made to the origin - response = await client.request({ + // Send request within the threshold + await client.request({ ...request, headers: { 'cache-control': 'min-fresh=5' } }) - strictEqual(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') + equal(requestsToOrigin, 1) - // Fast forward more. Response has 2sec TTL left after + // Fast forward again, response has 2sec ttl clock.tick(6000) - // Send the second request again, this time it shouldn't be within the - // threshold and a request should be made to the origin. - response = await client.request({ + await client.request({ ...request, headers: { 'cache-control': 'min-fresh=5' } }) - strictEqual(requestsToOrigin, 2) - strictEqual(await response.body.text(), 'asd') + equal(requestsToOrigin, 2) }) test('no-cache', async () => { let requestsToOrigin = 0 + let revalidationRequests = 0 const server = createServer((req, res) => { - if (requestsToOrigin === 1) { - notEqual(req.headers['if-modified-since'], undefined) + if (req.headers['if-modified-since']) { + revalidationRequests++ res.statusCode = 304 res.end() } else { + requestsToOrigin++ res.setHeader('cache-control', 'public, s-maxage=100') res.end('asd') } - - requestsToOrigin++ }).listen(0) const client = new Client(`http://localhost:${server.address().port}`) @@ -1038,7 +872,7 @@ describe('Cache Interceptor', () => { strictEqual(requestsToOrigin, 0) // Send initial request. This should reach the origin - let response = await client.request({ + await client.request({ origin: 'localhost', method: 'GET', path: '/', @@ -1047,10 +881,10 @@ describe('Cache Interceptor', () => { } }) strictEqual(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') + strictEqual(revalidationRequests, 0) // Send second request, a validation request should be sent - response = await client.request({ + await client.request({ origin: 'localhost', method: 'GET', path: '/', @@ -1058,27 +892,27 @@ describe('Cache Interceptor', () => { 'cache-control': 'no-cache' } }) - strictEqual(requestsToOrigin, 2) - strictEqual(await response.body.text(), 'asd') + strictEqual(requestsToOrigin, 1) + strictEqual(revalidationRequests, 1) // Send third request w/o no-cache, this should be handled by the cache - response = await client.request({ + await client.request({ origin: 'localhost', method: 'GET', path: '/' }) - strictEqual(requestsToOrigin, 2) - strictEqual(await response.body.text(), 'asd') + strictEqual(requestsToOrigin, 1) + strictEqual(revalidationRequests, 1) }) test('no-store', async () => { - const server = createServer((req, res) => { + const server = createServer((_, res) => { res.setHeader('cache-control', 'public, s-maxage=100') res.end('asd') }).listen(0) - const store = new cacheStores.MemoryCacheStore() - store.createWriteStream = (...args) => { + const store = new MemoryCacheStore() + store.createWriteStream = () => { fail('shouln\'t have reached this') } @@ -1092,8 +926,7 @@ describe('Cache Interceptor', () => { await once(server, 'listening') - // Send initial request. This should reach the origin - const response = await client.request({ + await client.request({ origin: 'localhost', method: 'GET', path: '/', @@ -1101,7 +934,6 @@ describe('Cache Interceptor', () => { 'cache-control': 'no-store' } }) - strictEqual(await response.body.text(), 'asd') }) test('only-if-cached', async () => { @@ -1123,16 +955,15 @@ describe('Cache Interceptor', () => { await once(server, 'listening') // Send initial request. This should reach the origin - let response = await client.request({ + await client.request({ origin: 'localhost', method: 'GET', path: '/' }) equal(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') // Send second request, this shouldn't reach the origin - response = await client.request({ + await client.request({ origin: 'localhost', method: 'GET', path: '/', @@ -1141,29 +972,32 @@ describe('Cache Interceptor', () => { } }) equal(requestsToOrigin, 1) - strictEqual(await response.body.text(), 'asd') // Send third request to an uncached resource, this should return a 504 - response = await client.request({ - origin: 'localhost', - method: 'GET', - path: '/bla', - headers: { - 'cache-control': 'only-if-cached' - } - }) - equal(response.statusCode, 504) + { + const res = await client.request({ + origin: 'localhost', + method: 'GET', + path: '/bla', + headers: { + 'cache-control': 'only-if-cached' + } + }) + equal(res.statusCode, 504) + } // Send fourth request to an uncached resource w/ a , this should return a 504 - response = await client.request({ - origin: 'localhost', - method: 'DELETE', - path: '/asd123', - headers: { - 'cache-control': 'only-if-cached' - } - }) - equal(response.statusCode, 504) + { + const res = await client.request({ + origin: 'localhost', + method: 'GET', + path: '/asd123', + headers: { + 'cache-control': 'only-if-cached' + } + }) + equal(res.statusCode, 504) + } }) test('stale-if-error', async () => { diff --git a/types/index.d.ts b/types/index.d.ts index bd3c1d47b23..3174b324200 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -64,6 +64,7 @@ declare namespace Undici { const caches: typeof import('./cache').caches const interceptors: typeof import('./interceptors').default const cacheStores: { - MemoryCacheStore: typeof import('./cache-interceptor').default.MemoryCacheStore + MemoryCacheStore: typeof import('./cache-interceptor').default.MemoryCacheStore, + SqliteCacheStore: typeof import('./cache-interceptor').default.SqliteCacheStore } }