supabase / supabase

The open source Firebase alternative.

Home Page:https://supabase.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Unable to use the Stripe generateTestHeaderString function in Edge Function unit tests

StefanVDWeide opened this issue · comments

Bug report

  • I confirm this is a bug with Supabase, not with my own application.
  • I confirm I have searched the Docs, GitHub Discussions, and Discord.

Describe the bug

When trying to use the generateTestHeaderString provide by the Stripe library in one of my edge function unit test, I keep running into the following error:

async => ./tests/stripe-webhook-test.ts:255:6
error: Error: SubtleCryptoProvider cannot be used in a synchronous context.
    at we.computeHMACSignature (https://esm.sh/v135/stripe@15.6.0/deno/stripe.mjs:3:2721)
    at Object.generateTestHeaderString (https://esm.sh/v135/stripe@15.6.0/deno/stripe.mjs:4:22239)
    at generateStripeHeader (x/supabase/functions/tests/utils/stripe-headers.ts:17:43)
    at testStripeWebhookCompletedCheckout (x/supabase/functions/tests/stripe-webhook-test.ts:199:27)

No matter if I wrap the stripe.webhooks.generateTestHeaderString() call in an async function and await it, the error will always be the same. I also tried const webCrypto = Stripe.createSubtleCryptoProvider(); and use the webCrypto object as a custom crypto provider as suggested in the docs for Stripe in edge functions, but this doesn't solve it either.

The solutions posed here also don't resolve this issue: stripe/stripe-node#1942

To Reproduce

Steps to reproduce the behavior, please provide code snippets or a repository:

  1. Create a unit test file in your supabase/functions/tests folder
  2. Add the following content
async function generateSignature(testPayload: any) {
  const payloadString = JSON.stringify(testPayload, null, 2);
  const secret = "whsec_test_secret"; 
  const webCrypto = Stripe.createSubtleCryptoProvider();

  const signature = stripe.webhooks.generateTestHeaderString({
    payload: payloadString,
    secret: secret,
    cryptoProvider: webCrypto,
  });

  return signature;
}


const testStripeWebhookCompletedCheckout = async () => {
  const client: SupabaseClient = createClient(
    supabaseUrl,
    supabaseServiceRoleKey,
    options,
  );

  const test_user_id = await createTestUser(client);
  const successEvent = {add Stripe event content here};

  const testPayload = {
    id: "evt_test_webhook",
    object: "event",
    data: successEvent,
  };

  const signature = await generateStripeHeader(STRIPE_API_KEY, testPayload);

  // Invoke the 'stripe-webhook' function with a parameter
  const { data: func_data, error: func_error } = await client.functions.invoke(
    "stripe-webhook",
    {
      body: successEvent,
      headers: {
        "stripe-signature": signature,
      },
    },
  );

  // Check for errors from the function invocation
  if (func_error) {
    throw new Error("Invalid response: " + func_error.message);
  }


Deno.test("testStripeWebhookCompletedCheckout", testStripeWebhookCompletedCheckout);
  1. Run the test using deno test --allow-all tests/stripe-webhook-test.ts
  2. See error

Expected behavior

I expect the headers to be generated using the crypto provider just as when running Stripe functions in edge functions.

System information

  • version of nuxt/supabase: 1.1.5
  • Version of supabase-js: 2.39.1
  • Version of Deno: 1.37.2

hey @StefanVDWeide

thanks for opening! To confirm, any of the solutions mentioned in the link do not work for you? Does this include using the constructEventAsync call outlined here?

Hey @encima

The solutions mentioned in the link indeed don't work. I also don't think the constructEventAsync is relevant in this case since that is to be used in the webhook function itself, which during testing works just fine. The generateTestHeaderString function I'm trying to use is supposed to generate test header so I can call the webhook at all from a unit test.

From the link you provided, I am trying to generate test headers in the unit test so that this part in the webhook works:

const signature = context.req.raw.headers.get("stripe-signature");

Apologies, I think my comment was not so clear!

Instead of using generateTestHeaderString, have you tried using constructEventAsync and passing the body, signature and secret to test the call.

Not sure I understand what you mean. We can not call the constructEventAsync without a valid signature, which is generated by the generateTestHeaderString function. As you can see in this example from the Stripe docs, we can only construct an event when we have valid header:

const payload = {
  id: 'evt_test_webhook',
  object: 'event',
};

const payloadString = JSON.stringify(payload, null, 2);
const secret = 'whsec_test_secret';

const header = stripe.webhooks.generateTestHeaderString({
  payload: payloadString,
  secret,
});

const event = stripe.webhooks.constructEvent(payloadString, header, secret);

// Do something with mocked signed event
expect(event.id).to.equal(payload.id);

For reference, this is the code of my Edge Function webhook that I'm trying to test. As you can see, we need the header for the constructEventAsync function call:

Deno.serve(async (request) => {
  if (request.method === "OPTIONS") {
    return new Response("ok", { headers: corsHeaders });
  }

  const signature = request.headers.get("Stripe-Signature");
  const body = await request.text();

  try {
    const event = (await stripe.webhooks.constructEventAsync(
      body,
      signature,
      STRIPE_WEBHOOK_SIGNING_SECRET,
      undefined,
      cryptoProvider,
    )) as Stripe.DiscriminatedEvent;
    const response = await handleStripeEvent(event);
    return new Response(JSON.stringify(response), {
      status: 200,
      headers: { "Content-Type": "application/json" },
    });
  } catch (error) {
    console.error(`Error handling request: ${error.message}`);
    return new Response(JSON.stringify({ error: error.message }), {
      status: error.status || 500,
    });
  }
});

Hopefully this clears things up!

Not sure I understand what you mean. We can not call the constructEventAsync without a valid signature, which is generated by the generateTestHeaderString function. As you can see in this example from the Stripe docs, we can only construct an event when we have valid header:

const payload = {

  id: 'evt_test_webhook',

  object: 'event',

};



const payloadString = JSON.stringify(payload, null, 2);

const secret = 'whsec_test_secret';



const header = stripe.webhooks.generateTestHeaderString({

  payload: payloadString,

  secret,

});



const event = stripe.webhooks.constructEvent(payloadString, header, secret);



// Do something with mocked signed event

expect(event.id).to.equal(payload.id);

For reference, this is the code of my Edge Function webhook that I'm trying to test. As you can see, we need the header for the constructEventAsync function call:

Deno.serve(async (request) => {

  if (request.method === "OPTIONS") {

    return new Response("ok", { headers: corsHeaders });

  }



  const signature = request.headers.get("Stripe-Signature");

  const body = await request.text();



  try {

    const event = (await stripe.webhooks.constructEventAsync(

      body,

      signature,

      STRIPE_WEBHOOK_SIGNING_SECRET,

      undefined,

      cryptoProvider,

    )) as Stripe.DiscriminatedEvent;

    const response = await handleStripeEvent(event);

    return new Response(JSON.stringify(response), {

      status: 200,

      headers: { "Content-Type": "application/json" },

    });

  } catch (error) {

    console.error(`Error handling request: ${error.message}`);

    return new Response(JSON.stringify({ error: error.message }), {

      status: error.status || 500,

    });

  }

});

Hopefully this clears things up!

This certainly does! Thanks for the extra insights and your patience explaining it. I was clearly in "triage" mode and had not looked deeply enough into it so I appreciate the explanations.

I'll let someone more knowledgeable chime in here 😅

No worries! And thank you for thinking along! :)