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:
- Create a unit test file in your
supabase/functions/tests
folder - 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);
- Run the test using
deno test --allow-all tests/stripe-webhook-test.ts
- 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 thegenerateTestHeaderString
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! :)