import assert from "assert" ; import fs from "fs/promises" ; import path from "path" ; import { setImmediate } from "timers/promises" ; import { CachePlugin } from "@miniflare/cache" ; import { BindingsPlugin , Fetcher , MiniflareCoreError , Response , _CoreMount , } from "@miniflare/core" ; import { Compatibility , NoOpLog , PluginContext , getRequestContext , } from "@miniflare/shared" ; import { getObjectProperties , logPluginOptions , parsePluginArgv , parsePluginWranglerConfig , useMiniflare , useServer , useTmp , } from "@miniflare/shared-test" ; import test from "ava" ; const log = new NoOpLog ( ) ; const compat = new Compatibility ( ) ; const rootPath = process . cwd ( ) ; const ctx = { log , compat , rootPath } ; const fixturesPath = path . join ( __dirname , ".." , ".." , ".." , "test" , "fixtures" ) ; const addModulePath = path . join ( fixturesPath , "add.wasm" ) ; test ( "BindingsPlugin: parses options from argv" , ( t ) => { let options = parsePluginArgv ( BindingsPlugin , [ "--env" , ".env.test" , "--binding" , "KEY1=value1" , "--binding" , "KEY2=value2" , "--global" , "KEY3=value3" , "--global" , "KEY4=value4" , "--wasm" , "MODULE1=module1.wasm" , "--wasm" , "MODULE2=module2.wasm" , "--service" , "SERVICE1=service1" , "--service" , "SERVICE2=service2@development" , ] ) ; t . deepEqual ( options , { envPath : ".env.test" , bindings : { KEY1 : "value1" , KEY2 : "value2" } , globals : { KEY3 : "value3" , KEY4 : "value4" } , wasmBindings : { MODULE1 : "module1.wasm" , MODULE2 : "module2.wasm" } , serviceBindings : { SERVICE1 : "service1" , SERVICE2 : { service : "service2" , environment : "development" } , } , } ) ; options = parsePluginArgv ( BindingsPlugin , [ "-e" , ".env.test" , "-b" , "KEY1=value1" , "-b" , "KEY2=value2" , "-S" , "SERVICE1=service1" , "-S" , "SERVICE2=service2@development" , ] ) ; t . deepEqual ( options , { envPath : ".env.test" , bindings : { KEY1 : "value1" , KEY2 : "value2" } , serviceBindings : { SERVICE1 : "service1" , SERVICE2 : { service : "service2" , environment : "development" } , } , } ) ; } ) ; test ( "BindingsPlugin: parses options from wrangler config" , async ( t ) => { let options = parsePluginWranglerConfig ( BindingsPlugin , { wasm_modules : { MODULE1 : "module1.wasm" , MODULE2 : "module2.wasm" , } , experimental_services : [ { name : "SERVICE1" , service : "service1" , environment : "development" } , { name : "SERVICE2" , service : "service2" , environment : "production" } , ] , miniflare : { globals : { KEY5 : "value5" , KEY6 : false , KEY7 : 10 } , env_path : ".env.test" , } , } ) ; t . like ( options , { envPath : ".env.test" , globals : { KEY5 : "value5" , KEY6 : false , KEY7 : 10 } , wasmBindings : { MODULE1 : "module1.wasm" , MODULE2 : "module2.wasm" } , serviceBindings : { SERVICE1 : { service : "service1" , environment : "development" } , SERVICE2 : { service : "service2" , environment : "production" } , } , } ) ; options = parsePluginWranglerConfig ( BindingsPlugin , { vars : { KEY1 : "value1" , KEY2 : "value2" , KEY3 : true , KEY4 : 42 } , } ) ; const plugin = new BindingsPlugin ( ctx , options ) ; const result = await plugin . setup ( ) ; t . deepEqual ( result . bindings , { KEY1 : "value1" , KEY2 : "value2" , KEY3 : "true" , KEY4 : "42" , } ) ; } ) ; test ( "BindingsPlugin: logs options" , ( t ) => { const wranglerOptions = parsePluginWranglerConfig ( BindingsPlugin , { vars : { KEY1 : "value1" , KEY2 : "value2" } , } ) ; let logs = logPluginOptions ( BindingsPlugin , { ... wranglerOptions , envPath : ".env.custom" , bindings : { KEY3 : "value3" , KEY4 : "value4" } , globals : { KEY5 : "value5" , KEY6 : "value6" } , wasmBindings : { MODULE1 : "module1.wasm" , MODULE2 : "module2.wasm" } , serviceBindings : { SERVICE1 : "service1" , SERVICE2 : { service : "service2" , environment : "development" } , } , } ) ; t . deepEqual ( logs , [ "Env Path: .env.custom" , "Wrangler Variables: KEY1, KEY2" , "Custom Bindings: KEY3, KEY4" , "Custom Globals: KEY5, KEY6" , "WASM Bindings: MODULE1, MODULE2" , "Service Bindings: SERVICE1, SERVICE2" , ] ) ; logs = logPluginOptions ( BindingsPlugin , { envPath : true } ) ; t . deepEqual ( logs , [ "Env Path: .env" ] ) ; logs = logPluginOptions ( BindingsPlugin , { envPath : false } ) ; t . deepEqual ( logs , [ ] ) ; } ) ; test ( "BindingsPlugin: uses default .env path if envPathDefaultFallback set and envPath is undefined" , ( t ) => { let plugin = new BindingsPlugin ( ctx , { envPathDefaultFallback : true } ) ; t . true ( plugin . envPath ) ; plugin = new BindingsPlugin ( ctx , { envPathDefaultFallback : true , envPath : false , } ) ; t . false ( plugin . envPath ) ; plugin = new BindingsPlugin ( ctx , { envPathDefaultFallback : true , envPath : ".env.custom" , } ) ; t . is ( plugin . envPath , ".env.custom" ) ; } ) ; test ( "BindingsPlugin: setup: loads .env bindings from default location" , async ( t ) => { const tmp = await useTmp ( t ) ; const defaultEnvPath = path . join ( tmp , ".env" ) ; let plugin = new BindingsPlugin ( { log , compat , rootPath : tmp } , { envPath : true } ) ; let result = await plugin . setup ( ) ; t . deepEqual ( result , { globals : undefined , bindings : { } , watch : [ defaultEnvPath ] , } ) ; await fs . writeFile ( defaultEnvPath , "KEY=value" ) ; result = await plugin . setup ( ) ; t . deepEqual ( result , { globals : undefined , bindings : { KEY : "value" } , watch : [ defaultEnvPath ] , } ) ; plugin = new BindingsPlugin ( { log , compat , rootPath : tmp } , { } ) ; result = await plugin . setup ( ) ; t . deepEqual ( result , { globals : undefined , bindings : { } , watch : [ ] } ) ; } ) ; test ( "BindingsPlugin: setup: loads .env bindings from custom location" , async ( t ) => { const tmp = await useTmp ( t ) ; const defaultEnvPath = path . join ( tmp , ".env" ) ; const customEnvPath = path . join ( tmp , ".env.custom" ) ; await fs . writeFile ( defaultEnvPath , "KEY=default" ) ; const plugin = new BindingsPlugin ( { log , compat , rootPath : tmp } , { envPath : ".env.custom" } ) ; await t . throwsAsync ( plugin . setup ( ) , { code : "ENOENT" , message : /\.env\.custom/ , } ) ; await fs . writeFile ( customEnvPath , "KEY=custom" ) ; const result = await plugin . setup ( ) ; t . deepEqual ( result , { globals : undefined , bindings : { KEY : "custom" } , watch : [ customEnvPath ] , } ) ; } ) ; test ( "BindingsPlugin: setup: includes custom bindings" , async ( t ) => { const obj = { a : 1 } ; const plugin = new BindingsPlugin ( ctx , { bindings : { obj } } ) ; const result = await plugin . setup ( ) ; t . is ( result . bindings ?. obj , obj ) ; t . deepEqual ( result . watch , [ ] ) ; } ) ; test ( "BindingsPlugin: setup: loads WebAssembly bindings" , async ( t ) => { let plugin = new BindingsPlugin ( ctx , { wasmBindings : { ADD : addModulePath } , } ) ; let result = await plugin . setup ( ) ; t . not ( result . bindings ?. ADD , undefined ) ; assert ( result . bindings ?. ADD ) ; const instance = new WebAssembly . Instance ( result . bindings . ADD ) ; assert ( typeof instance . exports . add === "function" ) ; t . is ( instance . exports . add ( 1 , 2 ) , 3 ) ; plugin = new BindingsPlugin ( { log , compat , rootPath : path . dirname ( addModulePath ) } , { wasmBindings : { ADD : path . basename ( addModulePath ) } } ) ; result = await plugin . setup ( ) ; t . not ( result . bindings ?. ADD , undefined ) ; } ) ; test ( "BindingsPlugin: setup: loads bindings from all sources" , async ( t ) => { const wranglerOptions = parsePluginWranglerConfig ( BindingsPlugin , { vars : { A : "w" , B : "w" , C : "w" , D : "w" , E : "w" } , } ) ; const tmp = await useTmp ( t ) ; const envPath = path . join ( tmp , ".env" ) ; await fs . writeFile ( envPath , "A=env\nB=env\nC=env\nD=env" ) ; const obj = { ping : "pong" } ; const throws = ( ) => { throw new Error ( "Should not be called" ) ; } ; const plugin = new BindingsPlugin ( ctx , { ... wranglerOptions , wasmBindings : { A : addModulePath , B : addModulePath , C : addModulePath , } , serviceBindings : { A : throws , B : throws } , bindings : { A : obj } , envPath , } ) ; const result = await plugin . setup ( ) ; assert ( result . bindings ) ; t . is ( result . bindings . E , "w" ) ; t . is ( result . bindings . D , "env" ) ; t . true ( result . bindings . C instanceof WebAssembly . Module ) ; t . true ( result . bindings . B instanceof Fetcher ) ; t . is ( result . bindings . A , obj ) ; } ) ; test ( "Fetcher: hides implementation details" , ( t ) => { const throws = ( ) => { throw new Error ( "Should not be called" ) ; } ; const fetcher = new Fetcher ( throws , throws ) ; t . deepEqual ( getObjectProperties ( fetcher ) , [ "fetch" ] ) ; } ) ; test ( "BindingsPlugin: dispatches fetch to mounted service" , async ( t ) => { const mf = useMiniflare ( { BindingsPlugin } , { name : "a" , modules : true , script : `export default {
        fetch(request, env) {
          const { pathname } = new URL(request.url);
          if (pathname === "/ping") {
            return new Response("pong");
          }
          return env.SERVICE_B.fetch("http://localhost/test", { method: "POST" });
        }
      }` , serviceBindings : { SERVICE_B : { service : "b" , environment : "production" } , } , mounts : { b : { name : "b" , modules : true , script : `export default {
            async fetch(request, env) {
              const res = await env.SERVICE_A.fetch("http://localhost/ping");
              const text = await res.text();
              return new Response(request.method + " " + request.url + ":" + text);
            }
          }` , serviceBindings : { SERVICE_A : "a" } , } , } , } ) ; const res = await mf . dispatchFetch ( "http://localhost/" ) ; t . is ( await res . text ( ) , "POST http://localhost/test:pong" ) ; } ) ; test ( "BindingsPlugin: dispatches fetch to custom service" , async ( t ) => { const plugin = new BindingsPlugin ( ctx , { serviceBindings : { async SERVICE ( request ) { return new Response ( `${ request . method } ${ request . url }` ) ; } , } , } ) ; const { bindings } = await plugin . setup ( ) ; let res = await bindings ! . SERVICE . fetch ( "http://localhost/" , { method : "POST" , } ) ; t . is ( await res . text ( ) , "POST http://localhost/" ) ; plugin . beforeReload ( ) ; plugin . reload ( { } , { } , new Map ( ) ) ; res = await bindings ! . SERVICE . fetch ( "http://localhost/test" ) ; t . is ( await res . text ( ) , "GET http://localhost/test" ) ; } ) ; test ( "BindingsPlugin: waits for services before dispatching" , async ( t ) => { const plugin = new BindingsPlugin ( ctx , { serviceBindings : { SERVICE : { service : "a" } } , } ) ; const { bindings } = await plugin . setup ( ) ; plugin . beforeReload ( ) ; const res = bindings ! . SERVICE . fetch ( "http://localhost/" ) ; await setImmediate ( ) ; const mount = { dispatchFetch : async ( ) => new Response ( "a service" ) , } ; plugin . reload ( { } , { } , new Map ( [ [ "a" , mount ] ] ) ) ; t . is ( await ( await res ) . text ( ) , "a service" ) ; } ) ; test ( "BindingsPlugin: reload: throws if service isn't mounted" , async ( t ) => { let plugin = new BindingsPlugin ( ctx , { serviceBindings : { SERVICE : "a" } , } ) ; await plugin . setup ( ) ; plugin . beforeReload ( ) ; t . throws ( ( ) => plugin . reload ( { } , { } , new Map ( ) ) , { instanceOf : MiniflareCoreError , code : "ERR_SERVICE_NOT_MOUNTED" , message : 'Service "a" for binding "SERVICE" not found.\nMake sure "a" is mounted so Miniflare knows where to find it.' , } ) ; plugin = new BindingsPlugin ( ctx , { serviceBindings : { SERVICE : ( ) => new Response ( ) } , } ) ; await plugin . setup ( ) ; plugin . beforeReload ( ) ; plugin . reload ( { } , { } , new Map ( ) ) ; t . pass ( ) ; } ) ; test ( "BindingsPlugin: reloads service bindings used by mount when another mounted worker reloads" , async ( t ) => { const mf = useMiniflare ( { BindingsPlugin } , { mounts : { a : { modules : true , routes : [ "*" ] , script : `export default {
            async fetch(request, env) {
              const res = await env.SERVICE_B.fetch("http://localhost/");
              return new Response("a" + await res.text());              
            }
          }` , serviceBindings : { SERVICE_B : "b" } , } , b : { modules : true , script : `export default { fetch: () => new Response("b1") }` , } , } , } ) ; let res = await mf . dispatchFetch ( "http://localhost/" ) ; t . is ( await res . text ( ) , "ab1" ) ; const b = await mf . getMount ( "b" ) ; await b . setOptions ( { script : `export default { fetch: () => new Response("b2") }` , } ) ; res = await mf . dispatchFetch ( "http://localhost/" ) ; t . is ( await res . text ( ) , "ab2" ) ; } ) ; test ( "BindingsPlugin: passes through when service doesn't respond" , async ( t ) => { const upstream = ( await useServer ( t , ( req , res ) => res . end ( "upstream" ) ) ) . http ; const mf = useMiniflare ( { BindingsPlugin } , { name : "parent" , script : 'addEventListener("fetch", () => {})' , mounts : { a : { script : 'addEventListener("fetch", () => {})' } , b : { modules : true , script : `export default {
            fetch(request, env, ctx) {
              ctx.passThroughOnException();
              throw new Error("oops");
            }
          }` , } , c : { modules : true , routes : [ "*/*" ] , script : `export default {
            async fetch(request, env) {
              const { pathname } = new URL(request.url);
              const name = pathname === "/a" ? "SERVICE_A" 
                : pathname === "/b" ? "SERVICE_B" : "SERVICE_PARENT";
              const service = env[name];
              const res = await service.fetch(${ JSON . stringify ( upstream ) });
              return new Response(name + ":" + await res.text());
            }
          }` , serviceBindings : { SERVICE_A : "a" , SERVICE_B : "b" , SERVICE_PARENT : "parent" , } , } , } , } ) ; let res = await mf . dispatchFetch ( "http://localhost/a" ) ; t . is ( await res . text ( ) , "SERVICE_A:upstream" ) ; res = await mf . dispatchFetch ( "http://localhost/b" ) ; t . is ( await res . text ( ) , "SERVICE_B:upstream" ) ; res = await mf . dispatchFetch ( "http://localhost/parent" ) ; t . is ( await res . text ( ) , "SERVICE_PARENT:upstream" ) ; } ) ; test ( "BindingsPlugin: propagates error if service throws" , async ( t ) => { const mf = useMiniflare ( { BindingsPlugin } , { modules : true , script : `export default {
        async fetch(request, env) {
          await env.SERVICE.fetch(request);
          return new Response("body");
        } ,
      }` , serviceBindings : { SERVICE : "a" } , mounts : { a : { modules : true , script : `export default {
            fetch: () => { throw new Error("oops"); },
          }` , } , } , } ) ; await t . throwsAsync ( mf . dispatchFetch ( "http://localhost/" ) , { message : "oops" , } ) ; } ) ; test ( "BindingsPlugin: service fetch creates new request context" , async ( t ) => { const bindings = { assertSubrequests ( expected ) { t . is ( getRequestContext ( ) ?. subrequests , expected ) ; } , } ; const mf = useMiniflare ( { BindingsPlugin , CachePlugin } , { bindings , serviceBindings : { SERVICE : "a" } , modules : true , script : `export default {
        async fetch(request, env) {
          env.assertSubrequests(0);
          await caches.default.match("http://localhost/");
          env.assertSubrequests(1);
          return await env.SERVICE.fetch(request);
        },
      }` , mounts : { a : { bindings , modules : true , script : `export default {
            async fetch(request, env) {
              env.assertSubrequests(0);
              await caches.default.match("http://localhost/");
              env.assertSubrequests(1);
              
              const n = parseInt(new URL(request.url).searchParams.get("n"));
              await Promise.all(
                Array.from(Array(n)).map(() => caches.default.match("http://localhost/"))
              );
              return new Response("body");
            },
          }` , } , } , } ) ; await t . throwsAsync ( mf . dispatchFetch ( "http://localhost/?n=50" ) , { instanceOf : Error , message : /^Too many subrequests/ , } ) ; const res = await mf . dispatchFetch ( "http://localhost/?n=1" ) ; t . is ( await res . text ( ) , "body" ) ; } ) ; test ( "BindingsPlugin: service fetch increases pipeline depth" , async ( t ) => { const depths = [ ] ; const mf = useMiniflare ( { BindingsPlugin } , { bindings : { recordDepth ( ) { const ctx = getRequestContext ( ) ! ; depths . push ( [ ctx . requestDepth , ctx . pipelineDepth ] ) ; } , } , modules : true , name : "service" , serviceBindings : { SERVICE : "service" } , script : `export default {
        async fetch(request, env) {
          env.recordDepth();
        
          const url = new URL(request.url);
          const n = parseInt(url.searchParams.get("n") ?? "0");
          if (n === 0) return new Response("end");
          url.searchParams.set("n", n - 1);
          
          const res = await env.SERVICE.fetch(url);
          return new Response(\`\${n},\${await res.text()}\`);
        }
      }` , } ) ; const res = await mf . dispatchFetch ( "http://localhost/?n=3" ) ; t . is ( await res . text ( ) , "3,2,1,end" ) ; t . deepEqual ( depths , [ [ 1 , 1 ] , [ 1 , 2 ] , [ 1 , 3 ] , [ 1 , 4 ] , ] ) ; await mf . dispatchFetch ( "http://localhost/?n=31" ) ; await t . throwsAsync ( mf . dispatchFetch ( "http://localhost/?n=32" ) , { instanceOf : Error , message : /^Subrequest depth limit exceeded.+\nService bindings can recurse up to 32 times\./ , } ) ; } ) ;