@@ -2,10 +2,15 @@ import { mkdirSync, writeFileSync } from "node:fs";
22import path from "node:path" ;
33
44import type { BuildOptions } from "@opennextjs/aws/build/helper.js" ;
5+ import { OpenNextConfig } from "@opennextjs/aws/types/open-next.js" ;
56import mockFs from "mock-fs" ;
67import { afterAll , afterEach , beforeAll , describe , expect , test , vi } from "vitest" ;
8+ import type { Unstable_Config as WranglerConfig } from "wrangler" ;
9+ import { unstable_startWorker } from "wrangler" ;
710
8- import { getCacheAssets , populateCache } from "./populate-cache.js" ;
11+ import { ensureR2Bucket } from "../utils/ensure-r2-bucket.js" ;
12+ import { getCacheAssets , populateCache , PopulateCacheOptions } from "./populate-cache.js" ;
13+ import { WorkerEnvVar } from "./utils/helpers.js" ;
914
1015describe ( "getCacheAssets" , ( ) => {
1116 beforeAll ( ( ) => {
@@ -78,7 +83,37 @@ vi.mock("./utils/helpers.js", () => ({
7883 quoteShellMeta : vi . fn ( ( s ) => s ) ,
7984} ) ) ;
8085
86+ vi . mock ( "../utils/ensure-r2-bucket.js" ) ;
87+ vi . mock ( "wrangler" ) ;
88+
8189describe ( "populateCache" , ( ) => {
90+ // @ts -expect-error - Partial mock of OpenNextConfig for testing
91+ const buildOptions : BuildOptions = {
92+ appPath : "/test/app" ,
93+ outputDir : "/test/output" ,
94+ } ;
95+ const config : OpenNextConfig = {
96+ default : {
97+ override : {
98+ // @ts -expect-error - Use R2 incremental cache
99+ incrementalCache : "cf-r2-incremental-cache" ,
100+ } ,
101+ } ,
102+ } ;
103+ // @ts -expect-error - Partial mock of WranglerConfig for testing
104+ const wranglerConfig : WranglerConfig = {
105+ r2_buckets : [
106+ {
107+ binding : "NEXT_INC_CACHE_R2_BUCKET" ,
108+ bucket_name : "test-bucket" ,
109+ preview_bucket_name : "preview-bucket" ,
110+ jurisdiction : "eu" ,
111+ } ,
112+ ] ,
113+ } ;
114+ // @ts -expect-error - Use partial WorkerEnvVar for testing
115+ const envVars : WorkerEnvVar = { } ;
116+
82117 const setupMockFileSystem = ( ) => {
83118 mockFs ( {
84119 "/test/output" : {
@@ -95,85 +130,103 @@ describe("populateCache", () => {
95130 } ) ;
96131 } ;
97132
98- describe . each ( [ { target : "local" as const } , { target : "remote" as const } ] ) (
99- "R2 incremental cache" ,
100- ( { target } ) => {
101- afterEach ( ( ) => {
102- mockFs . restore ( ) ;
103- } ) ;
133+ describe ( "R2 incremental cache" , ( ) => {
134+ afterEach ( ( ) => {
135+ vi . resetAllMocks ( ) ;
136+ mockFs . restore ( ) ;
137+ } ) ;
104138
105- test ( target , async ( ) => {
106- const { runWrangler } = await import ( "./utils/run-wrangler.js" ) ;
139+ test . each < PopulateCacheOptions > ( [
140+ { target : "local" , shouldUsePreviewId : false } ,
141+ { target : "remote" , shouldUsePreviewId : false } ,
142+ { target : "remote" , shouldUsePreviewId : true } ,
143+ ] ) (
144+ `$target (shouldUsePreviewId: $shouldUsePreviewId) - starts worker and sends individual cache entries via FormData` ,
145+ async ( populateCacheOptions ) => {
146+ const bucketName =
147+ populateCacheOptions . target === "remote" && populateCacheOptions . shouldUsePreviewId
148+ ? "preview-bucket"
149+ : "test-bucket" ;
150+ const mockWorkerDispose = vi . fn ( ) ;
107151
108152 setupMockFileSystem ( ) ;
109- vi . mocked ( runWrangler ) . mockClear ( ) ;
110-
111- await populateCache (
112- {
113- outputDir : "/test/output" ,
114- } as BuildOptions ,
115- {
116- default : {
117- override : {
118- incrementalCache : "cf-r2-incremental-cache" ,
119- } ,
120- } ,
121- } as any , // eslint-disable-line @typescript-eslint/no-explicit-any
122- {
123- r2_buckets : [
124- {
125- binding : "NEXT_INC_CACHE_R2_BUCKET" ,
126- bucket_name : "test-bucket" ,
127- } ,
128- ] ,
129- } as any , // eslint-disable-line @typescript-eslint/no-explicit-any
130- { target, shouldUsePreviewId : false } ,
131- { } as any // eslint-disable-line @typescript-eslint/no-explicit-any
153+ // @ts -expect-error - Mock unstable_startWorker to return a mock worker instance
154+ vi . mocked ( unstable_startWorker ) . mockResolvedValueOnce ( {
155+ ready : Promise . resolve ( ) ,
156+ url : Promise . resolve ( new URL ( "http://localhost:12345" ) ) ,
157+ dispose : mockWorkerDispose ,
158+ } ) ;
159+ vi . mocked ( ensureR2Bucket ) . mockResolvedValueOnce ( { success : true , bucketName } ) ;
160+
161+ // Mock fetch to return a successful response for each individual entry.
162+ const fetchMock = vi . spyOn ( global , "fetch" ) . mockResolvedValue (
163+ new Response ( JSON . stringify ( { success : true } ) , {
164+ status : 200 ,
165+ headers : { "Content-Type" : "application/json" } ,
166+ } )
132167 ) ;
133168
134- expect ( runWrangler ) . toHaveBeenCalledWith (
135- expect . anything ( ) ,
136- expect . arrayContaining ( [ "r2 bulk put" , "test-bucket" ] ) ,
137- expect . objectContaining ( { target } )
138- ) ;
139- } ) ;
140-
141- test ( `${ target } using jurisdiction` , async ( ) => {
142- const { runWrangler } = await import ( "./utils/run-wrangler.js" ) ;
169+ await populateCache ( buildOptions , config , wranglerConfig , populateCacheOptions , envVars ) ;
143170
144- setupMockFileSystem ( ) ;
145- vi . mocked ( runWrangler ) . mockClear ( ) ;
146-
147- await populateCache (
148- {
149- outputDir : "/test/output" ,
150- } as BuildOptions ,
151- {
152- default : {
153- override : {
154- incrementalCache : "cf-r2-incremental-cache" ,
155- } ,
156- } ,
157- } as any , // eslint-disable-line @typescript-eslint/no-explicit-any
158- {
159- r2_buckets : [
160- {
161- binding : "NEXT_INC_CACHE_R2_BUCKET" ,
162- bucket_name : "test-bucket" ,
171+ expect ( unstable_startWorker ) . toHaveBeenCalledWith (
172+ expect . objectContaining ( {
173+ bindings : expect . objectContaining ( {
174+ R2 : expect . objectContaining ( {
175+ type : "r2_bucket" ,
176+ bucket_name : bucketName ,
163177 jurisdiction : "eu" ,
164- } ,
165- ] ,
166- } as any , // eslint-disable-line @typescript-eslint/no-explicit-any
167- { target, shouldUsePreviewId : false } ,
168- { } as any // eslint-disable-line @typescript-eslint/no-explicit-any
178+ } ) ,
179+ } ) ,
180+ dev : expect . objectContaining ( {
181+ remote : populateCacheOptions . target === "remote" ,
182+ } ) ,
183+ } )
169184 ) ;
170185
171- expect ( runWrangler ) . toHaveBeenCalledWith (
172- expect . anything ( ) ,
173- expect . arrayContaining ( [ "r2 bulk put" , "test-bucket" , "--jurisdiction eu" ] ) ,
174- expect . objectContaining ( { target } )
175- ) ;
176- } ) ;
177- }
178- ) ;
186+ if ( populateCacheOptions . target === "remote" ) {
187+ expect ( ensureR2Bucket ) . toHaveBeenCalledWith ( "/test/app" , bucketName , "eu" ) ;
188+ } else {
189+ expect ( ensureR2Bucket ) . not . toHaveBeenCalled ( ) ;
190+ }
191+
192+ expect ( fetchMock ) . toBeCalled ( ) ;
193+
194+ for ( const [ input , init ] of fetchMock . mock . calls ) {
195+ expect ( input ) . toBe ( "http://localhost:12345/populate" ) ;
196+ expect ( init ?. method ) . toBe ( "POST" ) ;
197+
198+ const formData = init ?. body ;
199+ if ( formData instanceof FormData ) {
200+ // Verify the body is FormData containing key and value fields.
201+ expect ( formData . get ( "key" ) ) . toBeTypeOf ( "string" ) ;
202+ expect ( formData . get ( "value" ) ) . toBeTypeOf ( "string" ) ;
203+ } else {
204+ expect . unreachable ( "Expected request body to be FormData" ) ;
205+ }
206+ }
207+
208+ // Verify worker was disposed after sending entries.
209+ expect ( mockWorkerDispose ) . toHaveBeenCalled ( ) ;
210+ }
211+ ) ;
212+
213+ test ( "remote - exits when bucket provisioning fails" , async ( ) => {
214+ setupMockFileSystem ( ) ;
215+ vi . mocked ( ensureR2Bucket ) . mockResolvedValueOnce ( { success : false } ) ;
216+
217+ const result = populateCache (
218+ buildOptions ,
219+ config ,
220+ wranglerConfig ,
221+ { target : "remote" , shouldUsePreviewId : false } ,
222+ envVars
223+ ) ;
224+
225+ await expect ( result ) . rejects . toThrow (
226+ 'Failed to provision remote R2 bucket "test-bucket" for binding "NEXT_INC_CACHE_R2_BUCKET".'
227+ ) ;
228+
229+ expect ( unstable_startWorker ) . not . toHaveBeenCalled ( ) ;
230+ } ) ;
231+ } ) ;
179232} ) ;
0 commit comments