Better workflow tests

This commit is contained in:
Konstantin Wohlwend 2025-09-03 10:33:10 -07:00
parent 68a44c05fa
commit 0482a07880
2 changed files with 24 additions and 9 deletions

View File

@ -125,6 +125,16 @@ export async function retryTransaction<T>(client: PrismaClient, fn: (tx: PrismaC
// serializable transactions are currently off by default, later we may turn them on
const enableSerializable = options.level === "serializable";
const isRetryablePrismaError = (e: unknown) => {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
return [
"P2028", // Serializable/repeatable read conflict
"P2034", // Transaction already closed (eg. timeout)
];
}
return false;
};
return await traceSpan('Prisma transaction', async (span) => {
const res = await Result.retry(async (attemptIndex) => {
return await traceSpan(`transaction attempt #${attemptIndex}`, async (attemptSpan) => {
@ -140,7 +150,7 @@ export async function retryTransaction<T>(client: PrismaClient, fn: (tx: PrismaC
// to other (nested) transactions failing
// however, we make an exception for "Transaction already closed", as those are (annoyingly) thrown on
// the actual query, not the $transaction function itself
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2028") { // Transaction already closed
if (isRetryablePrismaError(e)) {
throw new TransactionErrorThatShouldBeRetried(e);
}
throw new TransactionErrorThatShouldNotBeRetried(e);
@ -165,11 +175,7 @@ export async function retryTransaction<T>(client: PrismaClient, fn: (tx: PrismaC
if (e instanceof TransactionErrorThatShouldNotBeRetried) {
throw e.cause;
}
if ([
"Transaction failed due to a write conflict or a deadlock. Please retry your transaction",
"Transaction already closed: A commit cannot be executed on an expired transaction. The timeout for this transaction",
].some(s => e instanceof Prisma.PrismaClientKnownRequestError && e.message.includes(s))) {
// transaction timeout, retry
if (isRetryablePrismaError(e)) {
return Result.error(e);
}
throw e;

View File

@ -37,6 +37,15 @@ async function waitForMailboxSubject(mailbox: Mailbox, subject: string) {
throw new Error(`Message with subject ${subject} not found after 10 tries`);
}
async function waitForServerMetadataNotNull(userId: string, key: string) {
for (let i = 0; i < 10; 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 10 tries`);
}
test("onSignUp workflow sends email for client sign-up", async ({ expect }) => {
await Project.createAndSwitch();
const mailbox = await bumpEmailAddress({ unindexed: true });
@ -243,7 +252,7 @@ test("anonymous sign-up does not trigger; upgrade triggers workflow", async ({ e
const { userId } = await Auth.Password.signUpWithEmail({ password: "password" });
expect(userId).toEqual(anonUserId);
await wait(16_000);
await waitForServerMetadataNotNull(anonUserId, markerKey);
const me2 = await niceBackendFetch("/api/v1/users/me", { accessType: "server" });
expect(me2.body.is_anonymous).toBe(false);
expect(me2.body.server_metadata?.[markerKey]).toBe(me2.body.primary_email);
@ -269,7 +278,7 @@ test("workflow source changes take effect for subsequent sign-ups", async ({ exp
});
await bumpEmailAddress({ unindexed: true });
await Auth.Password.signUpWithEmail({ password: "password" });
await wait(16_000);
await waitForServerMetadataNotNull("me", markerKey);
const me1 = await niceBackendFetch("/api/v1/users/me", { accessType: "server" });
expect(me1.body.server_metadata?.[markerKey]).toBe("v1");
@ -287,7 +296,7 @@ test("workflow source changes take effect for subsequent sign-ups", async ({ exp
});
await bumpEmailAddress({ unindexed: true });
await Auth.Password.signUpWithEmail({ password: "password" });
await wait(16_000);
await waitForServerMetadataNotNull("me", markerKey);
const me2 = await niceBackendFetch("/api/v1/users/me", { accessType: "server" });
expect(me2.body.server_metadata?.[markerKey]).toBe("v2");
}, {