Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
2 tasks done
StefanVDWeide opened this issue May 11, 2024 · 6 comments
Open
2 tasks done
Labels
bug Something isn't working edge functions

Comments

@StefanVDWeide
Copy link

StefanVDWeide commented May 11, 2024

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
@StefanVDWeide StefanVDWeide added the bug Something isn't working label May 11, 2024
@encima
Copy link
Contributor

encima commented May 13, 2024

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?

@StefanVDWeide
Copy link
Author

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");

@encima
Copy link
Contributor

encima commented May 13, 2024

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.

@StefanVDWeide
Copy link
Author

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!

@encima
Copy link
Contributor

encima commented May 14, 2024

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 😅

@StefanVDWeide
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working edge functions
Projects
None yet
Development

No branches or pull requests

2 participants