From 233fc2f0526467264995e586ab4c2ec1facdd5df Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Thu, 4 Jul 2024 10:17:09 -0400 Subject: [PATCH] feat: use Netlify Durable Cache (#2510) * test: add missing coverage for route handler headers * chore: allow e2e fixtures to override site id * feat: specify `durable` cache-control directive This is gated behind a feature flag for now. I can't link to any public docs yet, but by the time you're reading this you should be able to find a section on "Durable caching" at https://docs.netlify.com. --- src/run/handlers/server.ts | 4 +- src/run/headers.test.ts | 255 +++++++++++++++++-- src/run/headers.ts | 21 +- tests/e2e/durable-cache.test.ts | 19 ++ tests/integration/run/server-handler.test.ts | 56 ++++ tests/utils/create-e2e-fixture.ts | 21 +- tests/utils/fixture.ts | 8 +- 7 files changed, 347 insertions(+), 37 deletions(-) create mode 100644 tests/e2e/durable-cache.test.ts create mode 100644 tests/integration/run/server-handler.test.ts diff --git a/src/run/handlers/server.ts b/src/run/handlers/server.ts index d7dd4c5c0a..1037b4777d 100644 --- a/src/run/handlers/server.ts +++ b/src/run/handlers/server.ts @@ -112,7 +112,9 @@ export default async (request: Request, context: FutureContext) => { await adjustDateHeader({ headers: response.headers, request, span, tracer, requestContext }) - setCacheControlHeaders(response.headers, request, requestContext) + const useDurableCache = + context.flags.get('serverless_functions_nextjs_durable_cache_disable') !== true + setCacheControlHeaders(response.headers, request, requestContext, useDurableCache) setCacheTagsHeaders(response.headers, requestContext) setVaryHeaders(response.headers, request, nextConfig) setCacheStatusHeader(response.headers) diff --git a/src/run/headers.test.ts b/src/run/headers.test.ts index a1932c023a..4419559e3b 100644 --- a/src/run/headers.test.ts +++ b/src/run/headers.test.ts @@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { type FixtureTestContext } from '../../tests/utils/contexts.js' import { generateRandomObjectID, startMockBlobStore } from '../../tests/utils/helpers.js' -import { createRequestContext } from './handlers/request-context.cjs' +import { createRequestContext, type RequestContext } from './handlers/request-context.cjs' import { setCacheControlHeaders, setVaryHeaders } from './headers.js' beforeEach(async (ctx) => { @@ -194,17 +194,234 @@ describe('headers', () => { describe('setCacheControlHeaders', () => { const defaultUrl = 'https://example.com' + describe('Durable Cache feature flag disabled', () => { + test('should set permanent, non-durable "netlify-cdn-cache-control" if "cache-control" is not set and "requestContext.usedFsRead" is truthy', () => { + const headers = new Headers() + const request = new Request(defaultUrl) + vi.spyOn(headers, 'set') + + const requestContext = createRequestContext() + requestContext.usedFsRead = true + + setCacheControlHeaders(headers, request, requestContext, false) + + expect(headers.set).toHaveBeenNthCalledWith( + 1, + 'cache-control', + 'public, max-age=0, must-revalidate', + ) + expect(headers.set).toHaveBeenNthCalledWith( + 2, + 'netlify-cdn-cache-control', + 'max-age=31536000', + ) + }) + + describe('route handler responses with a specified `revalidate` value', () => { + test('should set non-durable SWC=1yr with 1yr TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is `false` (GET)', () => { + const headers = new Headers() + const request = new Request(defaultUrl) + vi.spyOn(headers, 'set') + + const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false } + setCacheControlHeaders(headers, request, ctx, false) + + expect(headers.set).toHaveBeenCalledTimes(1) + expect(headers.set).toHaveBeenNthCalledWith( + 1, + 'netlify-cdn-cache-control', + 's-maxage=31536000, stale-while-revalidate=31536000', + ) + }) + + test('should set non-durable SWC=1yr with 1yr TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is `false` (HEAD)', () => { + const headers = new Headers() + const request = new Request(defaultUrl, { method: 'HEAD' }) + vi.spyOn(headers, 'set') + + const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false } + setCacheControlHeaders(headers, request, ctx, false) + + expect(headers.set).toHaveBeenCalledTimes(1) + expect(headers.set).toHaveBeenNthCalledWith( + 1, + 'netlify-cdn-cache-control', + 's-maxage=31536000, stale-while-revalidate=31536000', + ) + }) + + test('should set non-durable SWC=1yr with given TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is a number (GET)', () => { + const headers = new Headers() + const request = new Request(defaultUrl) + vi.spyOn(headers, 'set') + + const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: 7200 } + setCacheControlHeaders(headers, request, ctx, false) + + expect(headers.set).toHaveBeenCalledTimes(1) + expect(headers.set).toHaveBeenNthCalledWith( + 1, + 'netlify-cdn-cache-control', + 's-maxage=7200, stale-while-revalidate=31536000', + ) + }) + + test('should set non-durable SWC=1yr with 1yr TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is a number (HEAD)', () => { + const headers = new Headers() + const request = new Request(defaultUrl, { method: 'HEAD' }) + vi.spyOn(headers, 'set') + + const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: 7200 } + setCacheControlHeaders(headers, request, ctx, false) + + expect(headers.set).toHaveBeenCalledTimes(1) + expect(headers.set).toHaveBeenNthCalledWith( + 1, + 'netlify-cdn-cache-control', + 's-maxage=7200, stale-while-revalidate=31536000', + ) + }) + }) + }) + + describe('route handler responses with a specified `revalidate` value', () => { + test('should not set any headers if "cdn-cache-control" is present', () => { + const givenHeaders = { + 'cdn-cache-control': 'public, max-age=0, must-revalidate', + } + const headers = new Headers(givenHeaders) + const request = new Request(defaultUrl) + vi.spyOn(headers, 'set') + + const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false } + setCacheControlHeaders(headers, request, ctx, true) + + expect(headers.set).toHaveBeenCalledTimes(0) + }) + + test('should not set any headers if "netlify-cdn-cache-control" is present', () => { + const givenHeaders = { + 'netlify-cdn-cache-control': 'public, max-age=0, must-revalidate', + } + const headers = new Headers(givenHeaders) + const request = new Request(defaultUrl) + vi.spyOn(headers, 'set') + + const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false } + setCacheControlHeaders(headers, request, ctx, true) + + expect(headers.set).toHaveBeenCalledTimes(0) + }) + + test('should mark content as stale if "{netlify-,}cdn-cache-control" is not present and "x-nextjs-cache" is "STALE" (GET)', () => { + const givenHeaders = { + 'x-nextjs-cache': 'STALE', + } + const headers = new Headers(givenHeaders) + const request = new Request(defaultUrl) + vi.spyOn(headers, 'set') + + const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false } + setCacheControlHeaders(headers, request, ctx, true) + + expect(headers.set).toHaveBeenCalledTimes(1) + expect(headers.set).toHaveBeenNthCalledWith( + 1, + 'netlify-cdn-cache-control', + 'public, max-age=0, must-revalidate', + ) + }) + + test('should mark content as stale if "{netlify-,}cdn-cache-control" is not present and "x-nextjs-cache" is "STALE" (HEAD)', () => { + const givenHeaders = { + 'x-nextjs-cache': 'STALE', + } + const headers = new Headers(givenHeaders) + const request = new Request(defaultUrl) + vi.spyOn(headers, 'set') + + const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false } + setCacheControlHeaders(headers, request, ctx, true) + + expect(headers.set).toHaveBeenCalledTimes(1) + expect(headers.set).toHaveBeenNthCalledWith( + 1, + 'netlify-cdn-cache-control', + 'public, max-age=0, must-revalidate', + ) + }) + + test('should set durable SWC=1yr with 1yr TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is `false` (HEAD)', () => { + const headers = new Headers() + const request = new Request(defaultUrl, { method: 'HEAD' }) + vi.spyOn(headers, 'set') + + const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false } + setCacheControlHeaders(headers, request, ctx, true) + + expect(headers.set).toHaveBeenCalledTimes(1) + expect(headers.set).toHaveBeenNthCalledWith( + 1, + 'netlify-cdn-cache-control', + 's-maxage=31536000, stale-while-revalidate=31536000, durable', + ) + }) + + test('should set durable SWC=1yr with given TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is a number (GET)', () => { + const headers = new Headers() + const request = new Request(defaultUrl) + vi.spyOn(headers, 'set') + + const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: 7200 } + setCacheControlHeaders(headers, request, ctx, true) + + expect(headers.set).toHaveBeenCalledTimes(1) + expect(headers.set).toHaveBeenNthCalledWith( + 1, + 'netlify-cdn-cache-control', + 's-maxage=7200, stale-while-revalidate=31536000, durable', + ) + }) + + test('should set durable SWC=1yr with 1yr TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is a number (HEAD)', () => { + const headers = new Headers() + const request = new Request(defaultUrl, { method: 'HEAD' }) + vi.spyOn(headers, 'set') + + const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: 7200 } + setCacheControlHeaders(headers, request, ctx, true) + + expect(headers.set).toHaveBeenCalledTimes(1) + expect(headers.set).toHaveBeenNthCalledWith( + 1, + 'netlify-cdn-cache-control', + 's-maxage=7200, stale-while-revalidate=31536000, durable', + ) + }) + + test('should not set any headers on POST request', () => { + const headers = new Headers() + const request = new Request(defaultUrl, { method: 'POST' }) + vi.spyOn(headers, 'set') + + const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false } + setCacheControlHeaders(headers, request, ctx, true) + + expect(headers.set).toHaveBeenCalledTimes(0) + }) + }) + test('should not set any headers if "cache-control" is not set and "requestContext.usedFsRead" is not truthy', () => { const headers = new Headers() const request = new Request(defaultUrl) vi.spyOn(headers, 'set') - setCacheControlHeaders(headers, request, createRequestContext()) + setCacheControlHeaders(headers, request, createRequestContext(), true) expect(headers.set).toHaveBeenCalledTimes(0) }) - test('should set permanent "netlify-cdn-cache-control" if "cache-control" is not set and "requestContext.usedFsRead" is truthy', () => { + test('should set permanent, durable "netlify-cdn-cache-control" if "cache-control" is not set and "requestContext.usedFsRead" is truthy', () => { const headers = new Headers() const request = new Request(defaultUrl) vi.spyOn(headers, 'set') @@ -212,7 +429,7 @@ describe('headers', () => { const requestContext = createRequestContext() requestContext.usedFsRead = true - setCacheControlHeaders(headers, request, requestContext) + setCacheControlHeaders(headers, request, requestContext, true) expect(headers.set).toHaveBeenNthCalledWith( 1, @@ -222,7 +439,7 @@ describe('headers', () => { expect(headers.set).toHaveBeenNthCalledWith( 2, 'netlify-cdn-cache-control', - 'max-age=31536000', + 'max-age=31536000, durable', ) }) @@ -235,7 +452,7 @@ describe('headers', () => { const request = new Request(defaultUrl) vi.spyOn(headers, 'set') - setCacheControlHeaders(headers, request, createRequestContext()) + setCacheControlHeaders(headers, request, createRequestContext(), true) expect(headers.set).toHaveBeenCalledTimes(0) }) @@ -249,7 +466,7 @@ describe('headers', () => { const request = new Request(defaultUrl) vi.spyOn(headers, 'set') - setCacheControlHeaders(headers, request, createRequestContext()) + setCacheControlHeaders(headers, request, createRequestContext(), true) expect(headers.set).toHaveBeenCalledTimes(0) }) @@ -262,7 +479,7 @@ describe('headers', () => { const request = new Request(defaultUrl) vi.spyOn(headers, 'set') - setCacheControlHeaders(headers, request, createRequestContext()) + setCacheControlHeaders(headers, request, createRequestContext(), true) expect(headers.set).toHaveBeenNthCalledWith( 1, @@ -272,7 +489,7 @@ describe('headers', () => { expect(headers.set).toHaveBeenNthCalledWith( 2, 'netlify-cdn-cache-control', - 'public, max-age=0, must-revalidate', + 'public, max-age=0, must-revalidate, durable', ) }) @@ -284,7 +501,7 @@ describe('headers', () => { const request = new Request(defaultUrl, { method: 'HEAD' }) vi.spyOn(headers, 'set') - setCacheControlHeaders(headers, request, createRequestContext()) + setCacheControlHeaders(headers, request, createRequestContext(), true) expect(headers.set).toHaveBeenNthCalledWith( 1, @@ -294,7 +511,7 @@ describe('headers', () => { expect(headers.set).toHaveBeenNthCalledWith( 2, 'netlify-cdn-cache-control', - 'public, max-age=0, must-revalidate', + 'public, max-age=0, must-revalidate, durable', ) }) @@ -306,7 +523,7 @@ describe('headers', () => { const request = new Request(defaultUrl, { method: 'POST' }) vi.spyOn(headers, 'set') - setCacheControlHeaders(headers, request, createRequestContext()) + setCacheControlHeaders(headers, request, createRequestContext(), true) expect(headers.set).toHaveBeenCalledTimes(0) }) @@ -319,13 +536,13 @@ describe('headers', () => { const request = new Request(defaultUrl) vi.spyOn(headers, 'set') - setCacheControlHeaders(headers, request, createRequestContext()) + setCacheControlHeaders(headers, request, createRequestContext(), true) expect(headers.set).toHaveBeenNthCalledWith(1, 'cache-control', 'public') expect(headers.set).toHaveBeenNthCalledWith( 2, 'netlify-cdn-cache-control', - 'public, s-maxage=604800', + 'public, s-maxage=604800, durable', ) }) @@ -337,17 +554,17 @@ describe('headers', () => { const request = new Request(defaultUrl) vi.spyOn(headers, 'set') - setCacheControlHeaders(headers, request, createRequestContext()) + setCacheControlHeaders(headers, request, createRequestContext(), true) expect(headers.set).toHaveBeenNthCalledWith(1, 'cache-control', 'max-age=604800') expect(headers.set).toHaveBeenNthCalledWith( 2, 'netlify-cdn-cache-control', - 'max-age=604800, stale-while-revalidate=86400', + 'max-age=604800, stale-while-revalidate=86400, durable', ) }) - test('should set default "cache-control" header if it contains only "s-maxage" and "stale-whie-revalidate"', () => { + test('should set default "cache-control" header if it contains only "s-maxage" and "stale-while-revalidate"', () => { const givenHeaders = { 'cache-control': 's-maxage=604800, stale-while-revalidate=86400', } @@ -355,7 +572,7 @@ describe('headers', () => { const request = new Request(defaultUrl) vi.spyOn(headers, 'set') - setCacheControlHeaders(headers, request, createRequestContext()) + setCacheControlHeaders(headers, request, createRequestContext(), true) expect(headers.set).toHaveBeenNthCalledWith( 1, @@ -365,7 +582,7 @@ describe('headers', () => { expect(headers.set).toHaveBeenNthCalledWith( 2, 'netlify-cdn-cache-control', - 's-maxage=604800, stale-while-revalidate=86400', + 's-maxage=604800, stale-while-revalidate=86400, durable', ) }) }) diff --git a/src/run/headers.ts b/src/run/headers.ts index a5b80cc13b..7d356fd4dc 100644 --- a/src/run/headers.ts +++ b/src/run/headers.ts @@ -65,12 +65,6 @@ const omitHeaderValues = (header: string, values: string[]): string => { return filteredValues.join(', ') } -const mapHeaderValues = (header: string, callback: (value: string) => string): string => { - const headerValues = getHeaderValueArray(header) - const mappedValues = headerValues.map(callback) - return mappedValues.join(', ') -} - /** * Ensure the Netlify CDN varies on things that Next.js varies on, * e.g. i18n, preview mode, etc. @@ -219,7 +213,9 @@ export const setCacheControlHeaders = ( headers: Headers, request: Request, requestContext: RequestContext, + useDurableCache: boolean, ) => { + const durableCacheDirective = useDurableCache ? ', durable' : '' if ( typeof requestContext.routeHandlerRevalidate !== 'undefined' && ['GET', 'HEAD'].includes(request.method) && @@ -231,7 +227,7 @@ export const setCacheControlHeaders = ( // if we are serving already stale response, instruct edge to not attempt to cache that response headers.get('x-nextjs-cache') === 'STALE' ? 'public, max-age=0, must-revalidate' - : `s-maxage=${requestContext.routeHandlerRevalidate === false ? 31536000 : requestContext.routeHandlerRevalidate}, stale-while-revalidate=31536000` + : `s-maxage=${requestContext.routeHandlerRevalidate === false ? 31536000 : requestContext.routeHandlerRevalidate}, stale-while-revalidate=31536000${durableCacheDirective}` headers.set('netlify-cdn-cache-control', cdnCacheControl) return @@ -253,9 +249,12 @@ export const setCacheControlHeaders = ( // if we are serving already stale response, instruct edge to not attempt to cache that response headers.get('x-nextjs-cache') === 'STALE' ? 'public, max-age=0, must-revalidate' - : mapHeaderValues(cacheControl, (value) => - value === 'stale-while-revalidate' ? 'stale-while-revalidate=31536000' : value, - ) + : [ + ...getHeaderValueArray(cacheControl).map((value) => + value === 'stale-while-revalidate' ? 'stale-while-revalidate=31536000' : value, + ), + ...(useDurableCache ? ['durable'] : []), + ].join(', ') headers.set('cache-control', browserCacheControl || 'public, max-age=0, must-revalidate') headers.set('netlify-cdn-cache-control', cdnCacheControl) @@ -270,7 +269,7 @@ export const setCacheControlHeaders = ( ) { // handle CDN Cache Control on static files headers.set('cache-control', 'public, max-age=0, must-revalidate') - headers.set('netlify-cdn-cache-control', `max-age=31536000`) + headers.set('netlify-cdn-cache-control', `max-age=31536000${durableCacheDirective}`) } } diff --git a/tests/e2e/durable-cache.test.ts b/tests/e2e/durable-cache.test.ts new file mode 100644 index 0000000000..269203ba89 --- /dev/null +++ b/tests/e2e/durable-cache.test.ts @@ -0,0 +1,19 @@ +import { expect } from '@playwright/test' +import { test } from '../utils/playwright-helpers.js' + +// This fixture is deployed to a separate site with the feature flag enabled +test('sets cache-control `durable` directive when feature flag is enabled', async ({ + page, + durableCache, +}) => { + const response = await page.goto(durableCache.url) + const headers = response?.headers() || {} + + const h1 = page.locator('h1') + await expect(h1).toHaveText('Home') + + expect(headers['netlify-cdn-cache-control']).toBe( + 's-maxage=31536000, stale-while-revalidate=31536000, durable', + ) + expect(headers['cache-control']).toBe('public,max-age=0,must-revalidate') +}) diff --git a/tests/integration/run/server-handler.test.ts b/tests/integration/run/server-handler.test.ts new file mode 100644 index 0000000000..f649878850 --- /dev/null +++ b/tests/integration/run/server-handler.test.ts @@ -0,0 +1,56 @@ +import { getLogger } from 'lambda-local' +import { v4 } from 'uuid' +import { beforeEach, describe, expect, test, vi } from 'vitest' +import { type FixtureTestContext } from '../../utils/contexts.js' +import { createFixture, invokeFunction, runPlugin } from '../../utils/fixture.js' +import { generateRandomObjectID, startMockBlobStore } from '../../utils/helpers.js' + +// Disable the verbose logging of the lambda-local runtime +getLogger().level = 'alert' + +beforeEach(async (ctx) => { + // set for each test a new deployID and siteID + ctx.deployID = generateRandomObjectID() + ctx.siteID = v4() + vi.stubEnv('SITE_ID', ctx.siteID) + vi.stubEnv('DEPLOY_ID', ctx.deployID) + // hide debug logs in tests + vi.spyOn(console, 'debug').mockImplementation(() => {}) + + await startMockBlobStore(ctx) +}) + +describe('`serverless_functions_nextjs_durable_cache_disable` feature flag', () => { + test('uses durable cache when flag is nil', async (ctx) => { + await createFixture('simple', ctx) + await runPlugin(ctx) + + const { headers } = await invokeFunction(ctx, { + flags: { serverless_functions_nextjs_durable_cache_disable: undefined }, + }) + + expect(headers['netlify-cdn-cache-control']).toContain('durable') + }) + + test('uses durable cache when flag is `false`', async (ctx) => { + await createFixture('simple', ctx) + await runPlugin(ctx) + + const { headers } = await invokeFunction(ctx, { + flags: { serverless_functions_nextjs_durable_cache_disable: false }, + }) + + expect(headers['netlify-cdn-cache-control']).toContain('durable') + }) + + test('does not use durable cache when flag is `true`', async (ctx) => { + await createFixture('simple', ctx) + await runPlugin(ctx) + + const { headers } = await invokeFunction(ctx, { + flags: { serverless_functions_nextjs_durable_cache_disable: true }, + }) + + expect(headers['netlify-cdn-cache-control']).not.toContain('durable') + }) +}) diff --git a/tests/utils/create-e2e-fixture.ts b/tests/utils/create-e2e-fixture.ts index eab7dc4f79..f1724c8d6a 100644 --- a/tests/utils/create-e2e-fixture.ts +++ b/tests/utils/create-e2e-fixture.ts @@ -11,8 +11,9 @@ import { cpus } from 'os' import pLimit from 'p-limit' import { setNextVersionInFixture } from './next-version-helpers.mjs' -// This is the netlify testing application -export const SITE_ID = process.env.NETLIFY_SITE_ID ?? 'ee859ce9-44a7-46be-830b-ead85e445e53' +// https://app.netlify.com/sites/next-runtime-testing +const DEFAULT_SITE_ID = 'ee859ce9-44a7-46be-830b-ead85e445e53' +export const SITE_ID = process.env.NETLIFY_SITE_ID ?? DEFAULT_SITE_ID const NEXT_VERSION = process.env.NEXT_VERSION || 'latest' export interface DeployResult { @@ -39,6 +40,10 @@ interface E2EConfig { * Some fixtures might pin to non-latest CLI versions. This is used to verify the used CLI version matches expected one */ expectedCliVersion?: string + /** + * Site ID to deploy to. Defaults to the `NETLIFY_SITE_ID` environment variable or a default site. + */ + siteId?: string } /** @@ -258,12 +263,12 @@ async function verifyFixture(isolatedFixtureRoot: string, { expectedCliVersion } async function deploySite( isolatedFixtureRoot: string, - { packagePath, cwd = '' }: E2EConfig, + { packagePath, cwd = '', siteId = SITE_ID }: E2EConfig, ): Promise { console.log(`🚀 Building and deploying site...`) const outputFile = 'deploy-output.txt' - let cmd = `npx ntl deploy --build --site ${SITE_ID}` + let cmd = `npx ntl deploy --build --site ${siteId}` if (packagePath) { cmd += ` --filter ${packagePath}` @@ -273,7 +278,7 @@ async function deploySite( await execaCommand(cmd, { cwd: siteDir, all: true }).pipeAll?.(join(siteDir, outputFile)) const output = await readFile(join(siteDir, outputFile), 'utf-8') - const [url] = new RegExp(/https:.+runtime-testing\.netlify\.app/gm).exec(output) || [] + const [url] = new RegExp(/https:.+\.netlify\.app/gm).exec(output) || [] if (!url) { throw new Error('Could not extract the URL from the build logs') } @@ -423,4 +428,10 @@ export const fixtureFactories = { publishDirectory: 'apps/site/.next', smoke: true, }), + durableCache: () => + createE2EFixture('simple', { + // https://app.netlify.com/sites/next-runtime-testing-durable-cache + // This site has all the Durable Cache feature flags enabled. + siteId: 'a8ceaa01-86fd-4c9a-8563-3769560d452a', + }), } diff --git a/tests/utils/fixture.ts b/tests/utils/fixture.ts index 7063af22eb..4f725af6f2 100644 --- a/tests/utils/fixture.ts +++ b/tests/utils/fixture.ts @@ -322,6 +322,9 @@ export async function uploadBlobs(ctx: FixtureTestContext, blobsDir: string) { ) } +const DEFAULT_FLAGS = { + serverless_functions_nextjs_durable_cache_disable: true, +} /** * Execute the function with the provided parameters * @param ctx @@ -346,9 +349,11 @@ export async function invokeFunction( body?: unknown /** Environment variables that should be set during the invocation */ env?: Record + /** Feature flags that should be set during the invocation */ + flags?: Record } = {}, ) { - const { httpMethod, headers, body, url, env } = options + const { httpMethod, headers, flags, url, env } = options // now for the execution set the process working directory to the dist entry point const cwdMock = vi .spyOn(process, 'cwd') @@ -381,6 +386,7 @@ export async function invokeFunction( headers: headers || {}, httpMethod: httpMethod || 'GET', rawUrl: new URL(url || '/', 'https://example.netlify').href, + flags: flags ?? DEFAULT_FLAGS, }, lambdaFunc: { handler }, timeoutMs: 4_000,