@@ -12,8 +12,8 @@ export interface WorkerGlobalSetupOptions {
1212 */
1313 workerPrefix : string ;
1414 /**
15- * The secrets of the Worker besides `E2E_TEST_DSN`, as a map from the binding name to the name of the
16- * environment variable that holds the value.
15+ * The secrets of the Worker besides `E2E_TEST_DSN` and `E2E_TEST_WORKER_TOKEN` , as a map from the
16+ * binding name to the name of the environment variable that holds the value.
1717 */
1818 secrets ?: Record < string , string > ;
1919}
@@ -138,6 +138,11 @@ function getAppDir(config: FullConfig): string {
138138/**
139139 * Returns a Playwright global setup that deploys the built test app as a real Worker. The tests read
140140 * its URL from `E2E_TEST_WORKER_URL`, and {@link workerGlobalTeardown} deletes it again.
141+ *
142+ * The Worker also gets the secret `E2E_TEST_WORKER_TOKEN`, a random value that is new for every deploy
143+ * and that {@link fetchFromWorker} sends as `Authorization: Bearer <token>`. CI keeps its Workers after
144+ * the run, so a Worker that can spend money (for example with an LLM API key) must reject requests
145+ * without this token.
141146 */
142147export function createWorkerGlobalSetup ( options : WorkerGlobalSetupOptions ) : ( config : FullConfig ) => Promise < void > {
143148 return async config => {
@@ -153,8 +158,14 @@ export function createWorkerGlobalSetup(options: WorkerGlobalSetupOptions): (con
153158 throw new Error ( 'CLOUDFLARE_ACCOUNT_ID must be set to deploy the test worker.' ) ;
154159 }
155160
161+ process . env . E2E_TEST_WORKER_TOKEN = randomBytes ( 32 ) . toString ( 'hex' ) ;
162+
156163 const secrets : Record < string , string > = { } ;
157- for ( const [ binding , envName ] of Object . entries ( { E2E_TEST_DSN : 'E2E_TEST_DSN' , ...options . secrets } ) ) {
164+ for ( const [ binding , envName ] of Object . entries ( {
165+ E2E_TEST_DSN : 'E2E_TEST_DSN' ,
166+ E2E_TEST_WORKER_TOKEN : 'E2E_TEST_WORKER_TOKEN' ,
167+ ...options . secrets ,
168+ } ) ) {
158169 const value = process . env [ envName ] ;
159170 if ( ! value ) {
160171 throw new Error ( `${ envName } must be set to deploy the test worker.` ) ;
@@ -218,14 +229,21 @@ export function workerGlobalTeardown(config: FullConfig): void {
218229 * Workers Logs had no invocation for it. `status` is the status the Worker answers with. A Worker
219230 * that threw answers with status 500 and Cloudflare error code 1101, which sets it apart from a 500
220231 * that did not come from the Worker.
232+ *
233+ * The request carries the `E2E_TEST_WORKER_TOKEN` of {@link createWorkerGlobalSetup}, unless `init`
234+ * sets its own `Authorization` header.
221235 */
222236export async function fetchFromWorker ( url : string , status : number , init ?: RequestInit ) : Promise < string > {
223237 const deadline = Date . now ( ) + 60_000 ;
224238 let lastAnswer = 'no answer' ;
239+ const headers = new Headers ( init ?. headers ) ;
240+ if ( process . env . E2E_TEST_WORKER_TOKEN && ! headers . has ( 'Authorization' ) ) {
241+ headers . set ( 'Authorization' , `Bearer ${ process . env . E2E_TEST_WORKER_TOKEN } ` ) ;
242+ }
225243
226244 while ( Date . now ( ) < deadline ) {
227245 try {
228- const response = await fetch ( url , init ) ;
246+ const response = await fetch ( url , { ... init , headers } ) ;
229247 const body = await response . text ( ) ;
230248 // Cloudflare sends its error page as HTML to some clients (Node's fetch among them) and as
231249 // `error code: <code>` plain text to others, so the code is read from either format.
0 commit comments