diff --git a/apps/backend/src/lib/workflows.tsx b/apps/backend/src/lib/workflows.tsx index 655947fad..1817c1cbf 100644 --- a/apps/backend/src/lib/workflows.tsx +++ b/apps/backend/src/lib/workflows.tsx @@ -83,7 +83,7 @@ export async function compileWorkflowSource(source: string): Promise { diff --git a/apps/e2e/tests/backend/workflows.test.ts b/apps/e2e/tests/backend/workflows.test.ts index 3b62bd304..3b1a0485a 100644 --- a/apps/e2e/tests/backend/workflows.test.ts +++ b/apps/e2e/tests/backend/workflows.test.ts @@ -27,23 +27,25 @@ async function configureEmailAndWorkflow(workflowId: string, tsSource: string, e }); } +const waitRetries = 15; + async function waitForMailboxSubject(mailbox: Mailbox, subject: string) { - for (let i = 0; i < 15; i++) { + for (let i = 0; i < waitRetries; i++) { const messages = await mailbox.fetchMessages(); const message = messages.find((m) => m.subject === subject); if (message) return; await wait(1_000); } - throw new Error(`Message with subject ${subject} not found after 15 tries`); + throw new Error(`Message with subject ${subject} not found after ${waitRetries} tries`); } async function waitForServerMetadataNotNull(userId: string, key: string) { - for (let i = 0; i < 15; i++) { + for (let i = 0; i < waitRetries; i++) { const user = await niceBackendFetch(`/api/v1/users/${userId}`, { accessType: "server" }); if (user.body.server_metadata?.[key]) return; await wait(1_000); } - throw new Error(`Server metadata for user ${userId} with key ${key} not found after 15 tries`); + throw new Error(`Server metadata for user ${userId} with key ${key} not found after ${waitRetries} tries`); } test("onSignUp workflow sends email for client sign-up", async ({ expect }) => { @@ -54,8 +56,10 @@ test("onSignUp workflow sends email for client sign-up", async ({ expect }) => { await configureEmailAndWorkflow("wf-email", ` onSignUp(async (user) => { await stackApp.sendEmail({ userIds: [user.id], subject: ${JSON.stringify(subject)}, html: "

hi

" }); + + // schedule a callback as an example (we don't actually test whether it executed successfully) return scheduleCallback({ - scheduleAt: new Date(Date.now() + 120_000), + scheduleAt: new Date(Date.now() + 7_000), data: { "example": "data" }, callbackId: "my-callback", }); @@ -100,6 +104,61 @@ test("onSignUp workflow sends email for client sign-up", async ({ expect }) => { timeout: 40_000, }); +test("onSignUp workflow can schedule callbacks", async ({ expect }) => { + await Project.createAndSwitch(); + const mailbox = await bumpEmailAddress({ unindexed: true }); + const subject = `WF client signup ${crypto.randomUUID()}`; + + await configureEmailAndWorkflow("wf-email", ` + onSignUp(async (user) => { + return scheduleCallback({ + scheduleAt: new Date(Date.now() + 7_000), + data: { "userId": user.id }, + callbackId: "my-callback", + }); + }); + + registerCallback("my-callback", async (data) => { + await stackApp.sendEmail({ userIds: [data.userId], subject: ${JSON.stringify(subject)}, html: "

hi

" }); + }); + `); + + await Auth.Password.signUpWithEmail({ password: "password" }); + + // since we wait for the callback, add some extra time + await wait(10_000); + await waitForMailboxSubject(mailbox, subject); + + expect(await mailbox.fetchMessages()).toMatchInlineSnapshot(` + [ + MailboxMessage { + "attachments": [], + "body": { + "html": "http://localhost:12345/some-callback-url?code=%3Cstripped+query+param%3E", + "text": "http://localhost:12345/some-callback-url?code=%3Cstripped+query+param%3E", + }, + "from": "Test Project ", + "subject": "Verify your email at New Project", + "to": ["@stack-generated.example.com>"], +