@better-auth/test-utils 1.5.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ export {
2
+ createTestSuite,
3
+ type InsertRandomFn,
4
+ type TestEntry,
5
+ type TestSuiteStats,
6
+ } from "./create-test-suite";
7
+ export { type Logger, testAdapter } from "./test-adapter";
@@ -0,0 +1,384 @@
1
+ import type { Awaitable, BetterAuthOptions } from "@better-auth/core";
2
+ import type { DBAdapter } from "@better-auth/core/db/adapter";
3
+ import { deepmerge, initGetModelName } from "@better-auth/core/db/adapter";
4
+ import { TTY_COLORS } from "@better-auth/core/env";
5
+ import { getAuthTables } from "better-auth/db";
6
+ import { afterAll, beforeAll, describe } from "vitest";
7
+ import type { createTestSuite, TestSuiteStats } from "./create-test-suite";
8
+
9
+ export type Logger = {
10
+ info: (...args: any[]) => void;
11
+ success: (...args: any[]) => void;
12
+ warn: (...args: any[]) => void;
13
+ error: (...args: any[]) => void;
14
+ debug: (...args: any[]) => void;
15
+ };
16
+
17
+ export const testAdapter = async ({
18
+ adapter: getAdapter,
19
+ runMigrations,
20
+ overrideBetterAuthOptions,
21
+ additionalCleanups,
22
+ tests,
23
+ prefixTests,
24
+ onFinish,
25
+ customIdGenerator,
26
+ transformIdOutput,
27
+ }: {
28
+ /**
29
+ * A function that will return the adapter instance to test with.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * testAdapter({
34
+ * adapter: (options) => drizzleAdapter(drizzle(db), {
35
+ * schema: generateSchema(options),
36
+ * }),
37
+ * })
38
+ */
39
+ adapter: (
40
+ options: BetterAuthOptions,
41
+ ) => Awaitable<(options: BetterAuthOptions) => DBAdapter<BetterAuthOptions>>;
42
+ /**
43
+ * A function that will run the database migrations.
44
+ */
45
+ runMigrations: (betterAuthOptions: BetterAuthOptions) => Promise<void> | void;
46
+ /**
47
+ * Any potential better-auth options overrides.
48
+ */
49
+ overrideBetterAuthOptions?: (
50
+ betterAuthOptions: BetterAuthOptions,
51
+ ) => BetterAuthOptions;
52
+ /**
53
+ * By default we will cleanup all tables automatically,
54
+ * but if you have additional cleanup logic, you can pass it here.
55
+ *
56
+ * Such as deleting a DB file that could had been created.
57
+ */
58
+ additionalCleanups?: () => Promise<void> | void;
59
+ /**
60
+ * A test suite to run.
61
+ */
62
+ tests: ReturnType<ReturnType<typeof createTestSuite>>[];
63
+ /**
64
+ * A prefix to add to the test suite name.
65
+ */
66
+ prefixTests?: string;
67
+ /**
68
+ * Upon finish of the tests, this function will be called.
69
+ */
70
+ onFinish?: () => Promise<void> | void;
71
+ /**
72
+ * Custom ID generator function to be used by the helper functions. (such as `insertRandom`)
73
+ */
74
+ customIdGenerator?: () => any;
75
+ /**
76
+ * A function that will transform the ID output.
77
+ */
78
+ transformIdOutput?: (id: any) => any;
79
+ }) => {
80
+ const defaultBAOptions = {} satisfies BetterAuthOptions;
81
+ let betterAuthOptions = (() => {
82
+ return {
83
+ ...defaultBAOptions,
84
+ ...(overrideBetterAuthOptions?.(defaultBAOptions) || {}),
85
+ } satisfies BetterAuthOptions;
86
+ })();
87
+
88
+ let adapter: DBAdapter<BetterAuthOptions> = (
89
+ await getAdapter(betterAuthOptions)
90
+ )(betterAuthOptions);
91
+
92
+ const adapterName = adapter.options?.adapterConfig.adapterName;
93
+ const adapterId = adapter.options?.adapterConfig.adapterId || adapter.id;
94
+ const adapterDisplayName = adapterName || adapterId;
95
+
96
+ const refreshAdapter = async (betterAuthOptions: BetterAuthOptions) => {
97
+ adapter = (await getAdapter(betterAuthOptions))(betterAuthOptions);
98
+ };
99
+
100
+ /**
101
+ * A helper function to log to the console.
102
+ */
103
+ const log: Logger = (() => {
104
+ return {
105
+ info: (...args: any[]) =>
106
+ console.log(
107
+ `${TTY_COLORS.fg.blue}INFO ${TTY_COLORS.reset} [${adapterDisplayName}]`,
108
+ ...args,
109
+ ),
110
+ success: (...args: any[]) =>
111
+ console.log(
112
+ `${TTY_COLORS.fg.green}SUCCESS${TTY_COLORS.reset} [${adapterDisplayName}]`,
113
+ ...args,
114
+ ),
115
+ warn: (...args: any[]) =>
116
+ console.log(
117
+ `${TTY_COLORS.fg.yellow}WARN ${TTY_COLORS.reset} [${adapterDisplayName}]`,
118
+ ...args,
119
+ ),
120
+ error: (...args: any[]) =>
121
+ console.log(
122
+ `${TTY_COLORS.fg.red}ERROR ${TTY_COLORS.reset} [${adapterDisplayName}]`,
123
+ ...args,
124
+ ),
125
+ debug: (...args: any[]) =>
126
+ console.log(
127
+ `${TTY_COLORS.fg.magenta}DEBUG ${TTY_COLORS.reset} [${adapterDisplayName}]`,
128
+ ...args,
129
+ ),
130
+ };
131
+ })();
132
+
133
+ /**
134
+ * Cleanup function to remove all rows from the database.
135
+ */
136
+ const cleanup = async () => {
137
+ const start = performance.now();
138
+ await refreshAdapter(betterAuthOptions);
139
+ const getAllModels = getAuthTables(betterAuthOptions);
140
+
141
+ // Clean up all rows from all models
142
+ for (const model of Object.keys(getAllModels)) {
143
+ const getModelName = initGetModelName({
144
+ usePlural: adapter.options?.adapterConfig?.usePlural,
145
+ schema: getAllModels,
146
+ });
147
+ try {
148
+ const modelName = getModelName(model);
149
+ await adapter.deleteMany({ model: modelName, where: [] });
150
+ } catch (error) {
151
+ const msg = `Error while cleaning up all rows from ${model}`;
152
+ log.error(msg, error);
153
+ throw new Error(msg, {
154
+ cause: error,
155
+ });
156
+ }
157
+ }
158
+
159
+ // Run additional cleanups
160
+ try {
161
+ await additionalCleanups?.();
162
+ } catch (error) {
163
+ const msg = `Error while running additional cleanups`;
164
+ log.error(msg, error);
165
+ throw new Error(msg, {
166
+ cause: error,
167
+ });
168
+ }
169
+ await refreshAdapter(betterAuthOptions);
170
+ log.success(
171
+ `${TTY_COLORS.bright}CLEAN-UP${TTY_COLORS.reset} completed successfully (${(performance.now() - start).toFixed(3)}ms)`,
172
+ );
173
+ };
174
+
175
+ /**
176
+ * A function that will run the database migrations.
177
+ */
178
+ const migrate = async () => {
179
+ const start = performance.now();
180
+
181
+ try {
182
+ await runMigrations(betterAuthOptions);
183
+ } catch (error) {
184
+ const msg = `Error while running migrations`;
185
+ log.error(msg, error);
186
+ throw new Error(msg, {
187
+ cause: error,
188
+ });
189
+ }
190
+ log.success(
191
+ `${TTY_COLORS.bright}MIGRATIONS${TTY_COLORS.reset} completed successfully (${(performance.now() - start).toFixed(3)}ms)`,
192
+ );
193
+ };
194
+
195
+ return {
196
+ execute: () => {
197
+ describe(adapterDisplayName, async () => {
198
+ // Collect statistics from all test suites
199
+ const allSuiteStats: TestSuiteStats[] = [];
200
+
201
+ beforeAll(async () => {
202
+ await migrate();
203
+ }, 60000);
204
+
205
+ afterAll(async () => {
206
+ await cleanup();
207
+
208
+ // Display statistics summary
209
+ if (allSuiteStats.length > 0) {
210
+ const totalMigrations = allSuiteStats.reduce(
211
+ (sum, stats) => sum + stats.migrationCount,
212
+ 0,
213
+ );
214
+ const totalMigrationTime = allSuiteStats.reduce(
215
+ (sum, stats) => sum + stats.totalMigrationTime,
216
+ 0,
217
+ );
218
+ const totalTests = allSuiteStats.reduce(
219
+ (sum, stats) => sum + stats.testCount,
220
+ 0,
221
+ );
222
+ const totalDuration = allSuiteStats.reduce(
223
+ (sum, stats) => sum + stats.suiteDuration,
224
+ 0,
225
+ );
226
+
227
+ const dash = "─";
228
+ const separator = `${dash.repeat(80)}`;
229
+
230
+ console.log(`\n${TTY_COLORS.fg.cyan}${separator}`);
231
+ console.log(
232
+ `${TTY_COLORS.fg.cyan}${TTY_COLORS.bright}TEST SUITE STATISTICS SUMMARY${TTY_COLORS.reset}`,
233
+ );
234
+ console.log(
235
+ `${TTY_COLORS.fg.cyan}${separator}${TTY_COLORS.reset}\n`,
236
+ );
237
+
238
+ // Per-suite breakdown
239
+ for (const stats of allSuiteStats) {
240
+ const avgMigrationTime =
241
+ stats.migrationCount > 0
242
+ ? (stats.totalMigrationTime / stats.migrationCount).toFixed(2)
243
+ : "0.00";
244
+ console.log(
245
+ `${TTY_COLORS.fg.magenta}${stats.suiteName}${TTY_COLORS.reset}:`,
246
+ );
247
+ console.log(
248
+ ` Tests: ${TTY_COLORS.fg.green}${stats.testCount}${TTY_COLORS.reset}`,
249
+ );
250
+ console.log(
251
+ ` Migrations: ${TTY_COLORS.fg.yellow}${stats.migrationCount}${TTY_COLORS.reset} (avg: ${avgMigrationTime}ms)`,
252
+ );
253
+ console.log(
254
+ ` Total Migration Time: ${TTY_COLORS.fg.yellow}${stats.totalMigrationTime.toFixed(2)}ms${TTY_COLORS.reset}`,
255
+ );
256
+ console.log(
257
+ ` Suite Duration: ${TTY_COLORS.fg.blue}${stats.suiteDuration.toFixed(2)}ms${TTY_COLORS.reset}`,
258
+ );
259
+
260
+ // Display grouping statistics if available
261
+ if (stats.groupingStats) {
262
+ const {
263
+ totalGroups,
264
+ averageTestsPerGroup,
265
+ largestGroupSize,
266
+ smallestGroupSize,
267
+ groupsWithMultipleTests,
268
+ } = stats.groupingStats;
269
+ console.log(
270
+ ` Test Groups: ${TTY_COLORS.fg.cyan}${totalGroups}${TTY_COLORS.reset}`,
271
+ );
272
+ if (totalGroups > 0) {
273
+ console.log(
274
+ ` Avg Tests/Group: ${TTY_COLORS.fg.cyan}${averageTestsPerGroup.toFixed(2)}${TTY_COLORS.reset}`,
275
+ );
276
+ console.log(
277
+ ` Largest Group: ${TTY_COLORS.fg.cyan}${largestGroupSize}${TTY_COLORS.reset}`,
278
+ );
279
+ console.log(
280
+ ` Smallest Group: ${TTY_COLORS.fg.cyan}${smallestGroupSize}${TTY_COLORS.reset}`,
281
+ );
282
+ console.log(
283
+ ` Groups w/ Multiple Tests: ${TTY_COLORS.fg.cyan}${groupsWithMultipleTests}${TTY_COLORS.reset}`,
284
+ );
285
+ }
286
+ }
287
+
288
+ console.log("");
289
+ }
290
+
291
+ // Totals
292
+ const avgMigrationTime =
293
+ totalMigrations > 0
294
+ ? (totalMigrationTime / totalMigrations).toFixed(2)
295
+ : "0.00";
296
+
297
+ // Calculate total grouping statistics
298
+ const totalGroups = allSuiteStats.reduce(
299
+ (sum, stats) => sum + (stats.groupingStats?.totalGroups || 0),
300
+ 0,
301
+ );
302
+ const totalGroupsWithMultipleTests = allSuiteStats.reduce(
303
+ (sum, stats) =>
304
+ sum + (stats.groupingStats?.groupsWithMultipleTests || 0),
305
+ 0,
306
+ );
307
+ const totalTestsInGroups = allSuiteStats.reduce(
308
+ (sum, stats) =>
309
+ sum + (stats.groupingStats?.totalTestsInGroups || 0),
310
+ 0,
311
+ );
312
+ const avgTestsPerGroup =
313
+ totalGroups > 0 ? totalTestsInGroups / totalGroups : 0;
314
+
315
+ console.log(`${TTY_COLORS.fg.cyan}${separator}`);
316
+ console.log(
317
+ `${TTY_COLORS.fg.cyan}${TTY_COLORS.bright}TOTALS${TTY_COLORS.reset}`,
318
+ );
319
+ console.log(
320
+ ` Total Tests: ${TTY_COLORS.fg.green}${totalTests}${TTY_COLORS.reset}`,
321
+ );
322
+ console.log(
323
+ ` Total Migrations: ${TTY_COLORS.fg.yellow}${totalMigrations}${TTY_COLORS.reset} (avg: ${avgMigrationTime}ms)`,
324
+ );
325
+ console.log(
326
+ ` Total Migration Time: ${TTY_COLORS.fg.yellow}${totalMigrationTime.toFixed(2)}ms${TTY_COLORS.reset}`,
327
+ );
328
+ console.log(
329
+ ` Total Duration: ${TTY_COLORS.fg.blue}${totalDuration.toFixed(2)}ms${TTY_COLORS.reset}`,
330
+ );
331
+
332
+ // Display total grouping statistics
333
+ if (totalGroups > 0) {
334
+ console.log(
335
+ ` Total Test Groups: ${TTY_COLORS.fg.cyan}${totalGroups}${TTY_COLORS.reset}`,
336
+ );
337
+ console.log(
338
+ ` Avg Tests/Group: ${TTY_COLORS.fg.cyan}${avgTestsPerGroup.toFixed(2)}${TTY_COLORS.reset}`,
339
+ );
340
+ console.log(
341
+ ` Groups w/ Multiple Tests: ${TTY_COLORS.fg.cyan}${totalGroupsWithMultipleTests}${TTY_COLORS.reset}`,
342
+ );
343
+ }
344
+
345
+ console.log(
346
+ `${TTY_COLORS.fg.cyan}${separator}${TTY_COLORS.reset}\n`,
347
+ );
348
+ }
349
+
350
+ await onFinish?.();
351
+ }, 60000);
352
+
353
+ for (const testSuite of tests) {
354
+ await testSuite({
355
+ adapter: async () => {
356
+ await refreshAdapter(betterAuthOptions);
357
+ return adapter;
358
+ },
359
+ adapterDisplayName,
360
+ log,
361
+ getBetterAuthOptions: () => betterAuthOptions,
362
+ modifyBetterAuthOptions: async (options) => {
363
+ const newOptions = deepmerge(defaultBAOptions, options);
364
+ betterAuthOptions = deepmerge(
365
+ newOptions,
366
+ overrideBetterAuthOptions?.(newOptions) || {},
367
+ );
368
+ await refreshAdapter(betterAuthOptions);
369
+ return betterAuthOptions;
370
+ },
371
+ cleanup,
372
+ prefixTests,
373
+ runMigrations: migrate,
374
+ onTestFinish: async (stats: TestSuiteStats) => {
375
+ allSuiteStats.push(stats);
376
+ },
377
+ customIdGenerator,
378
+ transformIdOutput,
379
+ });
380
+ }
381
+ });
382
+ },
383
+ };
384
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "references": [
4
+ {
5
+ "path": "../core/tsconfig.json"
6
+ },
7
+ {
8
+ "path": "../better-auth/tsconfig.json"
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ dts: { build: true, incremental: true },
5
+ format: ["esm"],
6
+ entry: {
7
+ adapter: "./src/adapter/index.ts",
8
+ },
9
+ unbundle: true,
10
+ outDir: "./dist",
11
+ clean: true,
12
+ });