mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-18 11:32:15 -05:00
perf(test-utils): stop re-deleting cleaned rows — up to 5.4× faster (#10762)
Co-authored-by: starslingdev[bot] <248995740+starslingdev[bot]@users.noreply.github.com> Co-authored-by: Taesu <bytaesu@gmail.com>
This commit is contained in:
co-authored by
starslingdev[bot] <248995740+starslingdev[bot]@users.noreply.github.com>
Taesu
parent
b95b7b31e7
commit
b7aeed84ce
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@better-auth/test-utils": patch
|
||||
---
|
||||
|
||||
Adapter test suites now run significantly faster. Cleanup between tests no longer re-deletes rows it has already removed, so suites spend far less time waiting on redundant database round trips.
|
||||
@@ -45,7 +45,7 @@ If you're using another runtime, keep your existing entry point and add the same
|
||||
<Callout type="info">
|
||||
The resulting Hono route must match your Better Auth `basePath`, which
|
||||
defaults to `/api/auth`. If your app already uses `new
|
||||
Hono().basePath("/api")`, mount Better Auth at `/auth/*`:
|
||||
Hono().basePath("/api")`, mount Better Auth at `/auth/*`:
|
||||
|
||||
```ts
|
||||
const app = new Hono().basePath("/api");
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"dev": "tsdown --watch"
|
||||
"dev": "tsdown --watch",
|
||||
"test": "vitest"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { CleanupResult } from "./cleanup";
|
||||
import { cleanupRows } from "./cleanup";
|
||||
|
||||
type TestRow = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
describe("cleanupRows", () => {
|
||||
it("does not retry rows that were deleted successfully", async () => {
|
||||
const deletedRowIds: string[] = [];
|
||||
let trackedRows: Record<string, TestRow[]> = {
|
||||
user: [{ id: "user-1" }, { id: "user-2" }],
|
||||
};
|
||||
const cleanupRow = async (
|
||||
_model: string,
|
||||
row: TestRow,
|
||||
): Promise<CleanupResult> => {
|
||||
deletedRowIds.push(row.id);
|
||||
return "complete";
|
||||
};
|
||||
|
||||
trackedRows = await cleanupRows(trackedRows, cleanupRow);
|
||||
trackedRows = await cleanupRows(trackedRows, cleanupRow);
|
||||
|
||||
expect(deletedRowIds).toEqual(["user-1", "user-2"]);
|
||||
expect(trackedRows).toEqual({});
|
||||
});
|
||||
|
||||
it("retries only rows whose deletion failed", async () => {
|
||||
const attemptedRowIds: string[] = [];
|
||||
let secondRowFails = true;
|
||||
let trackedRows: Record<string, TestRow[]> = {
|
||||
user: [{ id: "user-1" }, { id: "user-2" }, { id: "user-3" }],
|
||||
};
|
||||
const cleanupRow = async (
|
||||
_model: string,
|
||||
row: TestRow,
|
||||
): Promise<CleanupResult> => {
|
||||
attemptedRowIds.push(row.id);
|
||||
if (row.id === "user-2" && secondRowFails) {
|
||||
secondRowFails = false;
|
||||
throw new Error("Temporary delete failure");
|
||||
}
|
||||
return "complete";
|
||||
};
|
||||
|
||||
trackedRows = await cleanupRows(trackedRows, cleanupRow);
|
||||
|
||||
expect(trackedRows).toEqual({ user: [{ id: "user-2" }] });
|
||||
|
||||
trackedRows = await cleanupRows(trackedRows, cleanupRow);
|
||||
|
||||
expect(attemptedRowIds).toEqual(["user-1", "user-2", "user-3", "user-2"]);
|
||||
expect(trackedRows).toEqual({});
|
||||
});
|
||||
|
||||
it("retains rows explicitly marked for retry", async () => {
|
||||
let modelAvailable = false;
|
||||
let trackedRows: Record<string, TestRow[]> = {
|
||||
customModel: [{ id: "custom-1" }],
|
||||
};
|
||||
const cleanupRow = async (): Promise<CleanupResult> =>
|
||||
modelAvailable ? "complete" : "retry";
|
||||
|
||||
trackedRows = await cleanupRows(trackedRows, cleanupRow);
|
||||
|
||||
expect(trackedRows).toEqual({ customModel: [{ id: "custom-1" }] });
|
||||
|
||||
modelAvailable = true;
|
||||
trackedRows = await cleanupRows(trackedRows, cleanupRow);
|
||||
|
||||
expect(trackedRows).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
export type RowsByModel<Row> = Record<string, Row[]>;
|
||||
|
||||
export type CleanupResult = "complete" | "retry";
|
||||
|
||||
export async function cleanupRows<Row>(
|
||||
trackedRows: Readonly<Record<string, readonly Row[]>>,
|
||||
cleanupRow: (model: string, row: Row) => Promise<CleanupResult>,
|
||||
): Promise<RowsByModel<Row>> {
|
||||
const rowsToRetry: RowsByModel<Row> = {};
|
||||
|
||||
for (const [model, rows] of Object.entries(trackedRows)) {
|
||||
const failedRows: Row[] = [];
|
||||
for (const row of rows) {
|
||||
try {
|
||||
const result = await cleanupRow(model, row);
|
||||
if (result === "retry") {
|
||||
failedRows.push(row);
|
||||
}
|
||||
} catch {
|
||||
failedRows.push(row);
|
||||
}
|
||||
}
|
||||
if (failedRows.length > 0) {
|
||||
rowsToRetry[model] = failedRows;
|
||||
}
|
||||
}
|
||||
|
||||
return rowsToRetry;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
Verification,
|
||||
} from "@better-auth/core/db";
|
||||
import { createLocalAccountIssuer, getAuthTables } from "@better-auth/core/db";
|
||||
import type { DBAdapter } from "@better-auth/core/db/adapter";
|
||||
import type { DBAdapter, Where } from "@better-auth/core/db/adapter";
|
||||
import {
|
||||
createAdapterFactory,
|
||||
deepmerge,
|
||||
@@ -17,6 +17,8 @@ import { generateId } from "@better-auth/core/utils/id";
|
||||
import type { Auth } from "better-auth";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { test } from "vitest";
|
||||
import type { RowsByModel } from "./cleanup";
|
||||
import { cleanupRows } from "./cleanup";
|
||||
import type { Logger } from "./test-adapter";
|
||||
|
||||
/**
|
||||
@@ -122,6 +124,10 @@ type Failure<E> = {
|
||||
|
||||
type Result<T, E = Error> = Success<T> | Failure<E>;
|
||||
|
||||
type CreatedRow = {
|
||||
id: Where["value"];
|
||||
};
|
||||
|
||||
async function tryCatch<T, E = Error>(
|
||||
promise: Promise<T>,
|
||||
): Promise<Result<T, E>> {
|
||||
@@ -251,7 +257,7 @@ export const createTestSuite = <
|
||||
customIdGenerator?: () => any | Promise<any> | undefined;
|
||||
transformIdOutput?: (id: any) => string | undefined;
|
||||
}) => {
|
||||
const createdRows: Record<string, any[]> = {};
|
||||
let createdRows: RowsByModel<CreatedRow> = {};
|
||||
|
||||
let adapter = await helpers.adapter();
|
||||
const wrapperAdapter = (
|
||||
@@ -352,7 +358,7 @@ export const createTestSuite = <
|
||||
select,
|
||||
forceAllowId: true,
|
||||
});
|
||||
createdRows[model] = [...(createdRows[model] || []), res];
|
||||
(createdRows[model] ??= []).push({ id: res.id });
|
||||
return res as any;
|
||||
},
|
||||
options: adapter.options,
|
||||
@@ -374,34 +380,28 @@ export const createTestSuite = <
|
||||
};
|
||||
|
||||
const cleanupCreatedRows = async () => {
|
||||
if (Object.keys(createdRows).length === 0) return;
|
||||
|
||||
adapter = await helpers.adapter();
|
||||
for (const model of Object.keys(createdRows)) {
|
||||
for (const row of createdRows[model]!) {
|
||||
const schema = getAuthTables(helpers.getBetterAuthOptions());
|
||||
const getDefaultModelName = initGetDefaultModelName({
|
||||
schema,
|
||||
usePlural: adapter.options?.adapterConfig.usePlural,
|
||||
});
|
||||
let defaultModelName: string;
|
||||
try {
|
||||
defaultModelName = getDefaultModelName(model);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!schema[defaultModelName]) continue; // model doesn't exist in the schema anymore, so we skip it
|
||||
try {
|
||||
await adapter.delete({
|
||||
model,
|
||||
where: [{ field: "id", value: row.id }],
|
||||
});
|
||||
} catch {
|
||||
// We ignore any failed attempts to delete the created rows.
|
||||
}
|
||||
if (createdRows[model]!.length === 1) {
|
||||
delete createdRows[model];
|
||||
}
|
||||
const schema = getAuthTables(helpers.getBetterAuthOptions());
|
||||
const getDefaultModelName = initGetDefaultModelName({
|
||||
schema,
|
||||
usePlural: adapter.options?.adapterConfig.usePlural,
|
||||
});
|
||||
createdRows = await cleanupRows(createdRows, async (model, row) => {
|
||||
let defaultModelName: string;
|
||||
try {
|
||||
defaultModelName = getDefaultModelName(model);
|
||||
} catch {
|
||||
return "retry";
|
||||
}
|
||||
}
|
||||
if (!schema[defaultModelName]) return "retry";
|
||||
await adapter.delete({
|
||||
model,
|
||||
where: [{ field: "id", value: row.id }],
|
||||
});
|
||||
return "complete";
|
||||
});
|
||||
};
|
||||
|
||||
// Track current applied BetterAuth options state
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { defineProject } from "vitest/config";
|
||||
|
||||
export default defineProject({});
|
||||
Reference in New Issue
Block a user