@better-auth/test-utils 1.5.0-beta.18 → 1.5.0-beta.20
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.
- package/README.md +17 -0
- package/package.json +22 -6
- package/.turbo/turbo-build.log +0 -21
- package/src/adapter/create-test-suite.ts +0 -899
- package/src/adapter/index.ts +0 -7
- package/src/adapter/test-adapter.ts +0 -384
- package/tsconfig.json +0 -11
- package/tsdown.config.ts +0 -13
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Better Auth Test Utils
|
|
2
|
+
|
|
3
|
+
Testing utilities for [Better Auth](https://www.better-auth.com) adapter development and integration testing.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @better-auth/test-utils
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Documentation
|
|
12
|
+
|
|
13
|
+
For full documentation, visit [better-auth.com](https://www.better-auth.com).
|
|
14
|
+
|
|
15
|
+
## License
|
|
16
|
+
|
|
17
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/test-utils",
|
|
3
|
-
"version": "1.5.0-beta.
|
|
3
|
+
"version": "1.5.0-beta.20",
|
|
4
|
+
"description": "Testing utilities for Better Auth adapter development",
|
|
4
5
|
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://www.better-auth.com",
|
|
5
8
|
"repository": {
|
|
6
9
|
"type": "git",
|
|
7
|
-
"url": "https://github.com/better-auth/better-auth.git"
|
|
10
|
+
"url": "git+https://github.com/better-auth/better-auth.git",
|
|
11
|
+
"directory": "packages/test-utils"
|
|
8
12
|
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"auth",
|
|
15
|
+
"testing",
|
|
16
|
+
"typescript",
|
|
17
|
+
"better-auth"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
9
25
|
"exports": {
|
|
10
26
|
"./adapter": {
|
|
11
27
|
"dev-source": "./src/adapter.ts",
|
|
@@ -16,13 +32,13 @@
|
|
|
16
32
|
"devDependencies": {
|
|
17
33
|
"tsdown": "^0.20.3",
|
|
18
34
|
"vitest": "^4.0.18",
|
|
19
|
-
"@better-auth/core": "1.5.0-beta.
|
|
20
|
-
"better-auth": "1.5.0-beta.
|
|
35
|
+
"@better-auth/core": "1.5.0-beta.20",
|
|
36
|
+
"better-auth": "1.5.0-beta.20"
|
|
21
37
|
},
|
|
22
38
|
"peerDependencies": {
|
|
23
39
|
"vitest": "^4.0.18",
|
|
24
|
-
"@better-auth/core": "1.5.0-beta.
|
|
25
|
-
"better-auth": "1.5.0-beta.
|
|
40
|
+
"@better-auth/core": "1.5.0-beta.20",
|
|
41
|
+
"better-auth": "1.5.0-beta.20"
|
|
26
42
|
},
|
|
27
43
|
"scripts": {
|
|
28
44
|
"build": "tsdown",
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @better-auth/test-utils@1.5.0-beta.18 build /home/runner/work/better-auth/better-auth/packages/test-utils
|
|
3
|
-
> tsdown
|
|
4
|
-
|
|
5
|
-
[34mℹ[39m tsdown [2mv0.20.3[22m powered by rolldown [2mv1.0.0-rc.3[22m
|
|
6
|
-
[34mℹ[39m config file: [4m/home/runner/work/better-auth/better-auth/packages/test-utils/tsdown.config.ts[24m
|
|
7
|
-
[34mℹ[39m entry: [34msrc/adapter/index.ts[39m
|
|
8
|
-
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
|
-
[34mℹ[39m Build start
|
|
10
|
-
[34mℹ[39m [2mdist/[22m[1madapter.mjs[22m [2m 0.15 kB[22m [2m│ gzip: 0.10 kB[22m
|
|
11
|
-
[34mℹ[39m [2mdist/[22mcreate-test-suite.mjs.map [2m39.07 kB[22m [2m│ gzip: 9.66 kB[22m
|
|
12
|
-
[34mℹ[39m [2mdist/[22mtest-adapter.mjs.map [2m18.06 kB[22m [2m│ gzip: 4.23 kB[22m
|
|
13
|
-
[34mℹ[39m [2mdist/[22mcreate-test-suite.mjs [2m15.96 kB[22m [2m│ gzip: 3.98 kB[22m
|
|
14
|
-
[34mℹ[39m [2mdist/[22mtest-adapter.mjs [2m 8.12 kB[22m [2m│ gzip: 1.97 kB[22m
|
|
15
|
-
[34mℹ[39m [2mdist/[22m[32m[1madapter.d.mts[22m[39m [2m 0.27 kB[22m [2m│ gzip: 0.16 kB[22m
|
|
16
|
-
[34mℹ[39m [2mdist/[22m[32mcreate-test-suite.d.mts[39m [2m 5.13 kB[22m [2m│ gzip: 1.70 kB[22m
|
|
17
|
-
[34mℹ[39m [2mdist/[22m[32mtest-adapter.d.mts[39m [2m 2.21 kB[22m [2m│ gzip: 0.87 kB[22m
|
|
18
|
-
[34mℹ[39m 8 files, total: 88.97 kB
|
|
19
|
-
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
|
|
20
|
-
|
|
21
|
-
[32m✔[39m Build complete in [32m16224ms[39m
|
|
@@ -1,899 +0,0 @@
|
|
|
1
|
-
import type { BetterAuthOptions } from "@better-auth/core";
|
|
2
|
-
import type {
|
|
3
|
-
Account,
|
|
4
|
-
Session,
|
|
5
|
-
User,
|
|
6
|
-
Verification,
|
|
7
|
-
} from "@better-auth/core/db";
|
|
8
|
-
import { getAuthTables } from "@better-auth/core/db";
|
|
9
|
-
import type { DBAdapter } from "@better-auth/core/db/adapter";
|
|
10
|
-
import {
|
|
11
|
-
createAdapterFactory,
|
|
12
|
-
deepmerge,
|
|
13
|
-
initGetDefaultModelName,
|
|
14
|
-
} from "@better-auth/core/db/adapter";
|
|
15
|
-
import { TTY_COLORS } from "@better-auth/core/env";
|
|
16
|
-
import { generateId } from "@better-auth/core/utils/id";
|
|
17
|
-
import type { Auth } from "better-auth";
|
|
18
|
-
import { betterAuth } from "better-auth";
|
|
19
|
-
import { test } from "vitest";
|
|
20
|
-
import type { Logger } from "./test-adapter";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Test entry type that supports both callback and object formats.
|
|
24
|
-
* Object format allows specifying migration options that will be applied before the test runs.
|
|
25
|
-
*/
|
|
26
|
-
export type TestEntry =
|
|
27
|
-
| ((context: {
|
|
28
|
-
readonly skip: {
|
|
29
|
-
(note?: string | undefined): never;
|
|
30
|
-
(condition: boolean, note?: string | undefined): void;
|
|
31
|
-
};
|
|
32
|
-
}) => Promise<void>)
|
|
33
|
-
| {
|
|
34
|
-
migrateBetterAuth?: BetterAuthOptions;
|
|
35
|
-
test: (context: {
|
|
36
|
-
readonly skip: {
|
|
37
|
-
(note?: string | undefined): never;
|
|
38
|
-
(condition: boolean, note?: string | undefined): void;
|
|
39
|
-
};
|
|
40
|
-
}) => Promise<void>;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Deep equality comparison for BetterAuthOptions.
|
|
45
|
-
* Handles nested objects, arrays, and primitive values.
|
|
46
|
-
*/
|
|
47
|
-
function deepEqual(a: any, b: any): boolean {
|
|
48
|
-
if (a === b) return true;
|
|
49
|
-
if (a == null || b == null) return a === b;
|
|
50
|
-
if (typeof a !== typeof b) return false;
|
|
51
|
-
|
|
52
|
-
if (typeof a === "object") {
|
|
53
|
-
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
54
|
-
|
|
55
|
-
if (Array.isArray(a)) {
|
|
56
|
-
if (a.length !== b.length) return false;
|
|
57
|
-
for (let i = 0; i < a.length; i++) {
|
|
58
|
-
if (!deepEqual(a[i], b[i])) return false;
|
|
59
|
-
}
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const keysA = Object.keys(a);
|
|
64
|
-
const keysB = Object.keys(b);
|
|
65
|
-
|
|
66
|
-
if (keysA.length !== keysB.length) return false;
|
|
67
|
-
|
|
68
|
-
for (const key of keysA) {
|
|
69
|
-
if (!keysB.includes(key)) return false;
|
|
70
|
-
if (!deepEqual(a[key], b[key])) return false;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Statistics tracking for test suites.
|
|
81
|
-
*/
|
|
82
|
-
export type TestSuiteStats = {
|
|
83
|
-
migrationCount: number;
|
|
84
|
-
totalMigrationTime: number;
|
|
85
|
-
testCount: number;
|
|
86
|
-
suiteStartTime: number;
|
|
87
|
-
suiteDuration: number;
|
|
88
|
-
suiteName: string;
|
|
89
|
-
groupingStats?: {
|
|
90
|
-
totalGroups: number;
|
|
91
|
-
averageTestsPerGroup: number;
|
|
92
|
-
largestGroupSize: number;
|
|
93
|
-
smallestGroupSize: number;
|
|
94
|
-
groupsWithMultipleTests: number;
|
|
95
|
-
totalTestsInGroups: number;
|
|
96
|
-
};
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
type GenerateFn = <M extends "user" | "session" | "verification" | "account">(
|
|
100
|
-
Model: M,
|
|
101
|
-
) => Promise<
|
|
102
|
-
M extends "user"
|
|
103
|
-
? User
|
|
104
|
-
: M extends "session"
|
|
105
|
-
? Session
|
|
106
|
-
: M extends "verification"
|
|
107
|
-
? Verification
|
|
108
|
-
: M extends "account"
|
|
109
|
-
? Account
|
|
110
|
-
: undefined
|
|
111
|
-
>;
|
|
112
|
-
|
|
113
|
-
type Success<T> = {
|
|
114
|
-
data: T;
|
|
115
|
-
error: null;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
type Failure<E> = {
|
|
119
|
-
data: null;
|
|
120
|
-
error: E;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
type Result<T, E = Error> = Success<T> | Failure<E>;
|
|
124
|
-
|
|
125
|
-
async function tryCatch<T, E = Error>(
|
|
126
|
-
promise: Promise<T>,
|
|
127
|
-
): Promise<Result<T, E>> {
|
|
128
|
-
try {
|
|
129
|
-
const data = await promise;
|
|
130
|
-
return { data, error: null };
|
|
131
|
-
} catch (error) {
|
|
132
|
-
return { data: null, error: error as E };
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export type InsertRandomFn = <
|
|
137
|
-
M extends "user" | "session" | "verification" | "account",
|
|
138
|
-
Count extends number = 1,
|
|
139
|
-
>(
|
|
140
|
-
model: M,
|
|
141
|
-
count?: Count | undefined,
|
|
142
|
-
) => Promise<
|
|
143
|
-
Count extends 1
|
|
144
|
-
? M extends "user"
|
|
145
|
-
? [User]
|
|
146
|
-
: M extends "session"
|
|
147
|
-
? [User, Session]
|
|
148
|
-
: M extends "verification"
|
|
149
|
-
? [Verification]
|
|
150
|
-
: M extends "account"
|
|
151
|
-
? [User, Account]
|
|
152
|
-
: [undefined]
|
|
153
|
-
: Array<
|
|
154
|
-
M extends "user"
|
|
155
|
-
? [User]
|
|
156
|
-
: M extends "session"
|
|
157
|
-
? [User, Session]
|
|
158
|
-
: M extends "verification"
|
|
159
|
-
? [Verification]
|
|
160
|
-
: M extends "account"
|
|
161
|
-
? [User, Account]
|
|
162
|
-
: [undefined]
|
|
163
|
-
>
|
|
164
|
-
>;
|
|
165
|
-
|
|
166
|
-
export const createTestSuite = <
|
|
167
|
-
Tests extends Record<string, TestEntry>,
|
|
168
|
-
AdditionalOptions extends Record<string, any> = {},
|
|
169
|
-
>(
|
|
170
|
-
suiteName: string,
|
|
171
|
-
config: {
|
|
172
|
-
defaultBetterAuthOptions?: BetterAuthOptions | undefined;
|
|
173
|
-
/**
|
|
174
|
-
* Helpful if the default better auth options require migrations to be run.
|
|
175
|
-
*/
|
|
176
|
-
alwaysMigrate?: boolean | undefined;
|
|
177
|
-
prefixTests?: string | undefined;
|
|
178
|
-
customIdGenerator?: () => any | Promise<any> | undefined;
|
|
179
|
-
},
|
|
180
|
-
tests: (
|
|
181
|
-
helpers: {
|
|
182
|
-
adapter: DBAdapter;
|
|
183
|
-
log: Logger;
|
|
184
|
-
generate: GenerateFn;
|
|
185
|
-
insertRandom: InsertRandomFn;
|
|
186
|
-
/**
|
|
187
|
-
* A light cleanup function that will only delete rows it knows about.
|
|
188
|
-
*/
|
|
189
|
-
cleanup: () => Promise<void>;
|
|
190
|
-
/**
|
|
191
|
-
* A hard cleanup function that will delete all rows from the database.
|
|
192
|
-
*/
|
|
193
|
-
hardCleanup: () => Promise<void>;
|
|
194
|
-
modifyBetterAuthOptions: (
|
|
195
|
-
options: BetterAuthOptions,
|
|
196
|
-
shouldRunMigrations: boolean,
|
|
197
|
-
) => Promise<BetterAuthOptions>;
|
|
198
|
-
getBetterAuthOptions: () => BetterAuthOptions;
|
|
199
|
-
sortModels: (
|
|
200
|
-
models: Array<
|
|
201
|
-
Record<string, any> & {
|
|
202
|
-
id: string;
|
|
203
|
-
}
|
|
204
|
-
>,
|
|
205
|
-
by?: ("id" | "createdAt") | undefined,
|
|
206
|
-
) => (Record<string, any> & {
|
|
207
|
-
id: string;
|
|
208
|
-
})[];
|
|
209
|
-
getAuth: () => Promise<Auth>;
|
|
210
|
-
tryCatch<T, E = Error>(promise: Promise<T>): Promise<Result<T, E>>;
|
|
211
|
-
customIdGenerator?: () => any | Promise<any> | undefined;
|
|
212
|
-
transformIdOutput?: (id: any) => string | undefined;
|
|
213
|
-
/**
|
|
214
|
-
* Some adapters may change the ID type, this function allows you to pass the entire model
|
|
215
|
-
* data and it will return the correct better-auth-expected transformed data.
|
|
216
|
-
*
|
|
217
|
-
* Eg:
|
|
218
|
-
* MongoDB uses ObjectId for IDs, but it's possible the user can disable that option in the adapter config.
|
|
219
|
-
* Because of this, the expected data would be a string.
|
|
220
|
-
* These sorts of conversions will cause issues with the test when you use the `generate` function.
|
|
221
|
-
* This is because the `generate` function will return the raw data expected to be saved in DB, not the excpected BA output.
|
|
222
|
-
*/
|
|
223
|
-
transformGeneratedModel: (
|
|
224
|
-
data: Record<string, any>,
|
|
225
|
-
) => Record<string, any>;
|
|
226
|
-
},
|
|
227
|
-
additionalOptions?: AdditionalOptions | undefined,
|
|
228
|
-
) => Tests,
|
|
229
|
-
) => {
|
|
230
|
-
return (
|
|
231
|
-
options?:
|
|
232
|
-
| ({
|
|
233
|
-
disableTests?: Partial<
|
|
234
|
-
Record<keyof Tests, boolean> & { ALL?: boolean }
|
|
235
|
-
>;
|
|
236
|
-
} & AdditionalOptions)
|
|
237
|
-
| undefined,
|
|
238
|
-
) => {
|
|
239
|
-
return async (helpers: {
|
|
240
|
-
adapter: () => Promise<DBAdapter<BetterAuthOptions>>;
|
|
241
|
-
log: Logger;
|
|
242
|
-
adapterDisplayName: string;
|
|
243
|
-
getBetterAuthOptions: () => BetterAuthOptions;
|
|
244
|
-
modifyBetterAuthOptions: (
|
|
245
|
-
options: BetterAuthOptions,
|
|
246
|
-
) => Promise<BetterAuthOptions>;
|
|
247
|
-
cleanup: () => Promise<void>;
|
|
248
|
-
runMigrations: () => Promise<void>;
|
|
249
|
-
prefixTests?: string | undefined;
|
|
250
|
-
onTestFinish: (stats: TestSuiteStats) => Promise<void>;
|
|
251
|
-
customIdGenerator?: () => any | Promise<any> | undefined;
|
|
252
|
-
transformIdOutput?: (id: any) => string | undefined;
|
|
253
|
-
}) => {
|
|
254
|
-
const createdRows: Record<string, any[]> = {};
|
|
255
|
-
|
|
256
|
-
let adapter = await helpers.adapter();
|
|
257
|
-
const wrapperAdapter = (
|
|
258
|
-
overrideOptions?: BetterAuthOptions | undefined,
|
|
259
|
-
) => {
|
|
260
|
-
const options = deepmerge(
|
|
261
|
-
deepmerge(
|
|
262
|
-
helpers.getBetterAuthOptions(),
|
|
263
|
-
config?.defaultBetterAuthOptions || {},
|
|
264
|
-
),
|
|
265
|
-
overrideOptions || {},
|
|
266
|
-
);
|
|
267
|
-
const adapterConfig = {
|
|
268
|
-
adapterId: helpers.adapterDisplayName,
|
|
269
|
-
...(adapter.options?.adapterConfig || {}),
|
|
270
|
-
adapterName: `Wrapped ${adapter.options?.adapterConfig.adapterName}`,
|
|
271
|
-
disableTransformOutput: true,
|
|
272
|
-
disableTransformInput: true,
|
|
273
|
-
disableTransformJoin: true,
|
|
274
|
-
};
|
|
275
|
-
const adapterCreator = (
|
|
276
|
-
options: BetterAuthOptions,
|
|
277
|
-
): DBAdapter<BetterAuthOptions> =>
|
|
278
|
-
createAdapterFactory({
|
|
279
|
-
config: {
|
|
280
|
-
...adapterConfig,
|
|
281
|
-
transaction: adapter.transaction,
|
|
282
|
-
},
|
|
283
|
-
adapter: ({ getDefaultModelName }) => {
|
|
284
|
-
adapter.transaction = undefined as any;
|
|
285
|
-
return {
|
|
286
|
-
count: async (args: any) => {
|
|
287
|
-
adapter = await helpers.adapter();
|
|
288
|
-
const res = await adapter.count(args);
|
|
289
|
-
return res as any;
|
|
290
|
-
},
|
|
291
|
-
deleteMany: async (args: any) => {
|
|
292
|
-
adapter = await helpers.adapter();
|
|
293
|
-
const res = await adapter.deleteMany(args);
|
|
294
|
-
return res as any;
|
|
295
|
-
},
|
|
296
|
-
delete: async (args: any) => {
|
|
297
|
-
adapter = await helpers.adapter();
|
|
298
|
-
const res = await adapter.delete(args);
|
|
299
|
-
return res as any;
|
|
300
|
-
},
|
|
301
|
-
findOne: async (args) => {
|
|
302
|
-
adapter = await helpers.adapter();
|
|
303
|
-
const res = await adapter.findOne(args);
|
|
304
|
-
return res as any;
|
|
305
|
-
},
|
|
306
|
-
findMany: async (args) => {
|
|
307
|
-
adapter = await helpers.adapter();
|
|
308
|
-
const res = await adapter.findMany(args);
|
|
309
|
-
return res as any;
|
|
310
|
-
},
|
|
311
|
-
update: async (args: any) => {
|
|
312
|
-
adapter = await helpers.adapter();
|
|
313
|
-
const res = await adapter.update(args);
|
|
314
|
-
return res as any;
|
|
315
|
-
},
|
|
316
|
-
updateMany: async (args) => {
|
|
317
|
-
adapter = await helpers.adapter();
|
|
318
|
-
const res = await adapter.updateMany(args);
|
|
319
|
-
return res as any;
|
|
320
|
-
},
|
|
321
|
-
createSchema: adapter.createSchema as any,
|
|
322
|
-
async create({ data, model, select }) {
|
|
323
|
-
const defaultModelName = getDefaultModelName(model);
|
|
324
|
-
adapter = await helpers.adapter();
|
|
325
|
-
const res = await adapter.create({
|
|
326
|
-
data: data,
|
|
327
|
-
model: defaultModelName,
|
|
328
|
-
select,
|
|
329
|
-
forceAllowId: true,
|
|
330
|
-
});
|
|
331
|
-
createdRows[model] = [...(createdRows[model] || []), res];
|
|
332
|
-
return res as any;
|
|
333
|
-
},
|
|
334
|
-
options: adapter.options,
|
|
335
|
-
};
|
|
336
|
-
},
|
|
337
|
-
})(options);
|
|
338
|
-
|
|
339
|
-
return adapterCreator(options);
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
const resetDebugLogs = () => {
|
|
343
|
-
//@ts-expect-error
|
|
344
|
-
wrapperAdapter()?.adapterTestDebugLogs?.resetDebugLogs();
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
const printDebugLogs = () => {
|
|
348
|
-
//@ts-expect-error
|
|
349
|
-
wrapperAdapter()?.adapterTestDebugLogs?.printDebugLogs();
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
const cleanupCreatedRows = async () => {
|
|
353
|
-
adapter = await helpers.adapter();
|
|
354
|
-
for (const model of Object.keys(createdRows)) {
|
|
355
|
-
for (const row of createdRows[model]!) {
|
|
356
|
-
const schema = getAuthTables(helpers.getBetterAuthOptions());
|
|
357
|
-
const getDefaultModelName = initGetDefaultModelName({
|
|
358
|
-
schema,
|
|
359
|
-
usePlural: adapter.options?.adapterConfig.usePlural,
|
|
360
|
-
});
|
|
361
|
-
let defaultModelName: string;
|
|
362
|
-
try {
|
|
363
|
-
defaultModelName = getDefaultModelName(model);
|
|
364
|
-
} catch {
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
if (!schema[defaultModelName]) continue; // model doesn't exist in the schema anymore, so we skip it
|
|
368
|
-
try {
|
|
369
|
-
await adapter.delete({
|
|
370
|
-
model,
|
|
371
|
-
where: [{ field: "id", value: row.id }],
|
|
372
|
-
});
|
|
373
|
-
} catch {
|
|
374
|
-
// We ignore any failed attempts to delete the created rows.
|
|
375
|
-
}
|
|
376
|
-
if (createdRows[model]!.length === 1) {
|
|
377
|
-
delete createdRows[model];
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
// Track current applied BetterAuth options state
|
|
384
|
-
let currentAppliedOptions: BetterAuthOptions | null = null;
|
|
385
|
-
|
|
386
|
-
// Statistics tracking
|
|
387
|
-
const stats: TestSuiteStats = {
|
|
388
|
-
migrationCount: 0,
|
|
389
|
-
totalMigrationTime: 0,
|
|
390
|
-
testCount: 0,
|
|
391
|
-
suiteStartTime: performance.now(),
|
|
392
|
-
suiteDuration: 0,
|
|
393
|
-
suiteName,
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Apply BetterAuth options and run migrations if needed.
|
|
398
|
-
* Tracks migration statistics.
|
|
399
|
-
*/
|
|
400
|
-
const applyOptionsAndMigrate = async (
|
|
401
|
-
options: BetterAuthOptions | Partial<BetterAuthOptions>,
|
|
402
|
-
forceMigrate: boolean = false,
|
|
403
|
-
): Promise<BetterAuthOptions> => {
|
|
404
|
-
const finalOptions = deepmerge(
|
|
405
|
-
config?.defaultBetterAuthOptions || {},
|
|
406
|
-
options || {},
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
// Check if options have changed
|
|
410
|
-
const optionsChanged = !deepEqual(currentAppliedOptions, finalOptions);
|
|
411
|
-
|
|
412
|
-
if (optionsChanged || forceMigrate) {
|
|
413
|
-
adapter = await helpers.adapter();
|
|
414
|
-
await helpers.modifyBetterAuthOptions(finalOptions);
|
|
415
|
-
|
|
416
|
-
if (config.alwaysMigrate || forceMigrate) {
|
|
417
|
-
const migrationStart = performance.now();
|
|
418
|
-
await helpers.runMigrations();
|
|
419
|
-
const migrationTime = performance.now() - migrationStart;
|
|
420
|
-
|
|
421
|
-
stats.migrationCount++;
|
|
422
|
-
stats.totalMigrationTime += migrationTime;
|
|
423
|
-
|
|
424
|
-
adapter = await helpers.adapter();
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
currentAppliedOptions = finalOptions;
|
|
428
|
-
} else {
|
|
429
|
-
// Options haven't changed, just update the reference
|
|
430
|
-
currentAppliedOptions = finalOptions;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
return finalOptions;
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
const transformGeneratedModel = (data: Record<string, any>) => {
|
|
437
|
-
const newData = { ...data };
|
|
438
|
-
if (helpers.transformIdOutput) {
|
|
439
|
-
newData.id = helpers.transformIdOutput(newData.id);
|
|
440
|
-
}
|
|
441
|
-
return newData;
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
const idGenerator = async () => {
|
|
445
|
-
if (config.customIdGenerator) {
|
|
446
|
-
return config.customIdGenerator();
|
|
447
|
-
}
|
|
448
|
-
if (helpers.customIdGenerator) {
|
|
449
|
-
return helpers.customIdGenerator();
|
|
450
|
-
}
|
|
451
|
-
return generateId();
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
const generateModel: GenerateFn = async (model: string) => {
|
|
455
|
-
const id = await idGenerator();
|
|
456
|
-
const randomDate = new Date(
|
|
457
|
-
Date.now() - Math.random() * 1000 * 60 * 60 * 24 * 365,
|
|
458
|
-
);
|
|
459
|
-
if (model === "user") {
|
|
460
|
-
const user: User = {
|
|
461
|
-
id,
|
|
462
|
-
createdAt: randomDate,
|
|
463
|
-
updatedAt: new Date(),
|
|
464
|
-
email:
|
|
465
|
-
`user-${helpers.transformIdOutput?.(id) ?? id}@email.com`.toLowerCase(),
|
|
466
|
-
emailVerified: true,
|
|
467
|
-
name: `user-${helpers.transformIdOutput?.(id) ?? id}`,
|
|
468
|
-
image: null,
|
|
469
|
-
};
|
|
470
|
-
return user as any;
|
|
471
|
-
}
|
|
472
|
-
if (model === "session") {
|
|
473
|
-
const session: Session = {
|
|
474
|
-
id,
|
|
475
|
-
createdAt: randomDate,
|
|
476
|
-
updatedAt: new Date(),
|
|
477
|
-
expiresAt: new Date(),
|
|
478
|
-
token: generateId(32),
|
|
479
|
-
userId: generateId(),
|
|
480
|
-
ipAddress: "127.0.0.1",
|
|
481
|
-
userAgent: "Some User Agent",
|
|
482
|
-
};
|
|
483
|
-
return session as any;
|
|
484
|
-
}
|
|
485
|
-
if (model === "verification") {
|
|
486
|
-
const verification: Verification = {
|
|
487
|
-
id,
|
|
488
|
-
createdAt: randomDate,
|
|
489
|
-
updatedAt: new Date(),
|
|
490
|
-
expiresAt: new Date(),
|
|
491
|
-
identifier: `test:${generateId()}`,
|
|
492
|
-
value: generateId(),
|
|
493
|
-
};
|
|
494
|
-
return verification as any;
|
|
495
|
-
}
|
|
496
|
-
if (model === "account") {
|
|
497
|
-
const account: Account = {
|
|
498
|
-
id,
|
|
499
|
-
createdAt: randomDate,
|
|
500
|
-
updatedAt: new Date(),
|
|
501
|
-
accountId: generateId(),
|
|
502
|
-
providerId: "test",
|
|
503
|
-
userId: generateId(),
|
|
504
|
-
accessToken: generateId(),
|
|
505
|
-
refreshToken: generateId(),
|
|
506
|
-
idToken: generateId(),
|
|
507
|
-
accessTokenExpiresAt: new Date(),
|
|
508
|
-
refreshTokenExpiresAt: new Date(),
|
|
509
|
-
scope: "test",
|
|
510
|
-
};
|
|
511
|
-
return account as any;
|
|
512
|
-
}
|
|
513
|
-
// This should never happen given the type constraints, but TypeScript needs an exhaustive check
|
|
514
|
-
throw new Error(`Unknown model type: ${model}`);
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
const insertRandom: InsertRandomFn = async <
|
|
518
|
-
M extends "user" | "session" | "verification" | "account",
|
|
519
|
-
Count extends number = 1,
|
|
520
|
-
>(
|
|
521
|
-
model: M,
|
|
522
|
-
count: Count = 1 as Count,
|
|
523
|
-
) => {
|
|
524
|
-
const res: any[] = [];
|
|
525
|
-
const a = wrapperAdapter();
|
|
526
|
-
|
|
527
|
-
for (let i = 0; i < count; i++) {
|
|
528
|
-
const modelResults = [];
|
|
529
|
-
|
|
530
|
-
if (model === "user") {
|
|
531
|
-
const user = await generateModel("user");
|
|
532
|
-
modelResults.push(
|
|
533
|
-
await a.create({
|
|
534
|
-
data: user,
|
|
535
|
-
model: "user",
|
|
536
|
-
forceAllowId: true,
|
|
537
|
-
}),
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
if (model === "session") {
|
|
541
|
-
const user = await generateModel("user");
|
|
542
|
-
const userRes = await a.create({
|
|
543
|
-
data: user,
|
|
544
|
-
model: "user",
|
|
545
|
-
forceAllowId: true,
|
|
546
|
-
});
|
|
547
|
-
const session = await generateModel("session");
|
|
548
|
-
session.userId = userRes.id;
|
|
549
|
-
const sessionRes = await a.create({
|
|
550
|
-
data: session,
|
|
551
|
-
model: "session",
|
|
552
|
-
forceAllowId: true,
|
|
553
|
-
});
|
|
554
|
-
modelResults.push(userRes, sessionRes);
|
|
555
|
-
}
|
|
556
|
-
if (model === "verification") {
|
|
557
|
-
const verification = await generateModel("verification");
|
|
558
|
-
modelResults.push(
|
|
559
|
-
await a.create({
|
|
560
|
-
data: verification,
|
|
561
|
-
model: "verification",
|
|
562
|
-
forceAllowId: true,
|
|
563
|
-
}),
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
if (model === "account") {
|
|
567
|
-
const user = await generateModel("user");
|
|
568
|
-
const account = await generateModel("account");
|
|
569
|
-
const userRes = await a.create({
|
|
570
|
-
data: user,
|
|
571
|
-
model: "user",
|
|
572
|
-
forceAllowId: true,
|
|
573
|
-
});
|
|
574
|
-
account.userId = userRes.id;
|
|
575
|
-
const accRes = await a.create({
|
|
576
|
-
data: account,
|
|
577
|
-
model: "account",
|
|
578
|
-
forceAllowId: true,
|
|
579
|
-
});
|
|
580
|
-
modelResults.push(userRes, accRes);
|
|
581
|
-
}
|
|
582
|
-
res.push(modelResults);
|
|
583
|
-
}
|
|
584
|
-
return res.length === 1 ? res[0] : (res as any);
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
const sortModels = (
|
|
588
|
-
models: Array<Record<string, any> & { id: string }>,
|
|
589
|
-
by: "id" | "createdAt" = "id",
|
|
590
|
-
) => {
|
|
591
|
-
return models.sort((a, b) => {
|
|
592
|
-
if (by === "createdAt") {
|
|
593
|
-
return (
|
|
594
|
-
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
595
|
-
);
|
|
596
|
-
}
|
|
597
|
-
return a.id.localeCompare(b.id);
|
|
598
|
-
});
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
const modifyBetterAuthOptions = async (
|
|
602
|
-
opts: BetterAuthOptions,
|
|
603
|
-
shouldRunMigrations: boolean,
|
|
604
|
-
) => {
|
|
605
|
-
return await applyOptionsAndMigrate(opts, shouldRunMigrations);
|
|
606
|
-
};
|
|
607
|
-
|
|
608
|
-
const additionalOptions = { ...options };
|
|
609
|
-
additionalOptions.disableTests = undefined;
|
|
610
|
-
|
|
611
|
-
const fullTests = tests(
|
|
612
|
-
{
|
|
613
|
-
adapter: new Proxy({} as any, {
|
|
614
|
-
get(target, prop) {
|
|
615
|
-
const adapter = wrapperAdapter();
|
|
616
|
-
if (prop === "transaction") {
|
|
617
|
-
return adapter.transaction;
|
|
618
|
-
}
|
|
619
|
-
const value = adapter[prop as keyof typeof adapter];
|
|
620
|
-
if (typeof value === "function") {
|
|
621
|
-
return value.bind(adapter);
|
|
622
|
-
}
|
|
623
|
-
return value;
|
|
624
|
-
},
|
|
625
|
-
}),
|
|
626
|
-
getAuth: async () => {
|
|
627
|
-
adapter = await helpers.adapter();
|
|
628
|
-
const auth = betterAuth({
|
|
629
|
-
...helpers.getBetterAuthOptions(),
|
|
630
|
-
...(config?.defaultBetterAuthOptions || {}),
|
|
631
|
-
database: (options: BetterAuthOptions) => {
|
|
632
|
-
const adapter = wrapperAdapter(options);
|
|
633
|
-
return adapter;
|
|
634
|
-
},
|
|
635
|
-
} as BetterAuthOptions);
|
|
636
|
-
return auth;
|
|
637
|
-
},
|
|
638
|
-
log: helpers.log,
|
|
639
|
-
generate: generateModel,
|
|
640
|
-
cleanup: cleanupCreatedRows,
|
|
641
|
-
hardCleanup: helpers.cleanup,
|
|
642
|
-
insertRandom,
|
|
643
|
-
modifyBetterAuthOptions,
|
|
644
|
-
getBetterAuthOptions: helpers.getBetterAuthOptions,
|
|
645
|
-
sortModels,
|
|
646
|
-
tryCatch,
|
|
647
|
-
customIdGenerator: helpers.customIdGenerator,
|
|
648
|
-
transformGeneratedModel,
|
|
649
|
-
transformIdOutput: helpers.transformIdOutput,
|
|
650
|
-
},
|
|
651
|
-
additionalOptions as AdditionalOptions,
|
|
652
|
-
);
|
|
653
|
-
|
|
654
|
-
const dash = `─`;
|
|
655
|
-
const allDisabled: boolean = options?.disableTests?.ALL ?? false;
|
|
656
|
-
|
|
657
|
-
// Here to display a label in the tests showing the suite name
|
|
658
|
-
test(`\n${TTY_COLORS.fg.white}${" ".repeat(3)}${dash.repeat(35)} [${TTY_COLORS.fg.magenta}${suiteName}${TTY_COLORS.fg.white}] ${dash.repeat(35)}`, async () => {
|
|
659
|
-
try {
|
|
660
|
-
await helpers.cleanup();
|
|
661
|
-
} catch {}
|
|
662
|
-
if (config.defaultBetterAuthOptions && !allDisabled) {
|
|
663
|
-
await applyOptionsAndMigrate(
|
|
664
|
-
config.defaultBetterAuthOptions,
|
|
665
|
-
config.alwaysMigrate,
|
|
666
|
-
);
|
|
667
|
-
}
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
/**
|
|
671
|
-
* Extract test function and migration options from a test entry.
|
|
672
|
-
*/
|
|
673
|
-
const extractTestEntry = (
|
|
674
|
-
entry: TestEntry,
|
|
675
|
-
): {
|
|
676
|
-
testFn: (context: {
|
|
677
|
-
readonly skip: {
|
|
678
|
-
(note?: string | undefined): never;
|
|
679
|
-
(condition: boolean, note?: string | undefined): void;
|
|
680
|
-
};
|
|
681
|
-
}) => Promise<void>;
|
|
682
|
-
migrateBetterAuth?: BetterAuthOptions;
|
|
683
|
-
} => {
|
|
684
|
-
if (typeof entry === "function") {
|
|
685
|
-
return { testFn: entry };
|
|
686
|
-
}
|
|
687
|
-
return {
|
|
688
|
-
testFn: entry.test,
|
|
689
|
-
migrateBetterAuth: entry.migrateBetterAuth,
|
|
690
|
-
};
|
|
691
|
-
};
|
|
692
|
-
|
|
693
|
-
// Convert test entries to array with migration info (moved before onFinish for access)
|
|
694
|
-
const testEntries = Object.entries(fullTests).map(([name, entry]) => {
|
|
695
|
-
const { testFn, migrateBetterAuth } = extractTestEntry(
|
|
696
|
-
entry as TestEntry,
|
|
697
|
-
);
|
|
698
|
-
return { name, testFn, migrateBetterAuth };
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
/**
|
|
702
|
-
* Group tests by their migrateBetterAuth options.
|
|
703
|
-
* Tests with equal migration options are grouped together.
|
|
704
|
-
*/
|
|
705
|
-
type TestGroup = {
|
|
706
|
-
migrationOptions: BetterAuthOptions | null | undefined;
|
|
707
|
-
testIndices: number[];
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
const groupTestsByMigrationOptions = (): TestGroup[] => {
|
|
711
|
-
const groups: TestGroup[] = [];
|
|
712
|
-
let currentGroup: TestGroup | null = null;
|
|
713
|
-
|
|
714
|
-
for (let i = 0; i < testEntries.length; i++) {
|
|
715
|
-
const { migrateBetterAuth } = testEntries[i]!;
|
|
716
|
-
const isSkipped =
|
|
717
|
-
(allDisabled &&
|
|
718
|
-
options?.disableTests?.[testEntries[i]!.name] !== false) ||
|
|
719
|
-
(options?.disableTests?.[testEntries[i]!.name] ?? false);
|
|
720
|
-
|
|
721
|
-
// Skip grouping for skipped tests - they'll be handled individually
|
|
722
|
-
if (isSkipped) {
|
|
723
|
-
if (currentGroup) {
|
|
724
|
-
groups.push(currentGroup);
|
|
725
|
-
currentGroup = null;
|
|
726
|
-
}
|
|
727
|
-
groups.push({
|
|
728
|
-
migrationOptions: migrateBetterAuth,
|
|
729
|
-
testIndices: [i],
|
|
730
|
-
});
|
|
731
|
-
continue;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// Check if this test belongs to the current group
|
|
735
|
-
if (
|
|
736
|
-
currentGroup &&
|
|
737
|
-
deepEqual(currentGroup.migrationOptions, migrateBetterAuth)
|
|
738
|
-
) {
|
|
739
|
-
currentGroup.testIndices.push(i);
|
|
740
|
-
} else {
|
|
741
|
-
// Start a new group
|
|
742
|
-
if (currentGroup) {
|
|
743
|
-
groups.push(currentGroup);
|
|
744
|
-
}
|
|
745
|
-
currentGroup = {
|
|
746
|
-
migrationOptions: migrateBetterAuth,
|
|
747
|
-
testIndices: [i],
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Add the last group if it exists
|
|
753
|
-
if (currentGroup) {
|
|
754
|
-
groups.push(currentGroup);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
return groups;
|
|
758
|
-
};
|
|
759
|
-
|
|
760
|
-
const testGroups = groupTestsByMigrationOptions();
|
|
761
|
-
|
|
762
|
-
// Calculate grouping statistics
|
|
763
|
-
const calculateGroupingStats = () => {
|
|
764
|
-
const nonSkippedGroups = testGroups.filter(
|
|
765
|
-
(group) => group.testIndices.length > 0,
|
|
766
|
-
);
|
|
767
|
-
const groupSizes = nonSkippedGroups.map(
|
|
768
|
-
(group) => group.testIndices.length,
|
|
769
|
-
);
|
|
770
|
-
|
|
771
|
-
if (groupSizes.length === 0) {
|
|
772
|
-
return {
|
|
773
|
-
totalGroups: 0,
|
|
774
|
-
averageTestsPerGroup: 0,
|
|
775
|
-
largestGroupSize: 0,
|
|
776
|
-
smallestGroupSize: 0,
|
|
777
|
-
groupsWithMultipleTests: 0,
|
|
778
|
-
totalTestsInGroups: 0,
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
const totalTestsInGroups = groupSizes.reduce(
|
|
783
|
-
(sum, size) => sum + size,
|
|
784
|
-
0,
|
|
785
|
-
);
|
|
786
|
-
const groupsWithMultipleTests = groupSizes.filter(
|
|
787
|
-
(size) => size > 1,
|
|
788
|
-
).length;
|
|
789
|
-
|
|
790
|
-
return {
|
|
791
|
-
totalGroups: nonSkippedGroups.length,
|
|
792
|
-
averageTestsPerGroup: totalTestsInGroups / nonSkippedGroups.length,
|
|
793
|
-
largestGroupSize: Math.max(...groupSizes),
|
|
794
|
-
smallestGroupSize: Math.min(...groupSizes),
|
|
795
|
-
groupsWithMultipleTests,
|
|
796
|
-
totalTestsInGroups,
|
|
797
|
-
};
|
|
798
|
-
};
|
|
799
|
-
|
|
800
|
-
const onFinish = async (testName: string) => {
|
|
801
|
-
await cleanupCreatedRows();
|
|
802
|
-
|
|
803
|
-
const currentTestIndex = testEntries.findIndex(
|
|
804
|
-
({ name }) => name === testName,
|
|
805
|
-
);
|
|
806
|
-
const isLastTest = currentTestIndex === testEntries.length - 1;
|
|
807
|
-
|
|
808
|
-
if (isLastTest) {
|
|
809
|
-
stats.suiteDuration = performance.now() - stats.suiteStartTime;
|
|
810
|
-
stats.groupingStats = calculateGroupingStats();
|
|
811
|
-
await helpers.onTestFinish(stats);
|
|
812
|
-
}
|
|
813
|
-
};
|
|
814
|
-
|
|
815
|
-
// Track the current group's migration options
|
|
816
|
-
let currentGroupMigrationOptions: BetterAuthOptions | null | undefined =
|
|
817
|
-
null;
|
|
818
|
-
|
|
819
|
-
for (let i = 0; i < testEntries.length; i++) {
|
|
820
|
-
const { name: testName, testFn, migrateBetterAuth } = testEntries[i]!;
|
|
821
|
-
|
|
822
|
-
// Find which group this test belongs to
|
|
823
|
-
const testGroup = testGroups.find((group) =>
|
|
824
|
-
group.testIndices.includes(i),
|
|
825
|
-
);
|
|
826
|
-
const isFirstInGroup = testGroup && testGroup.testIndices[0] === i;
|
|
827
|
-
|
|
828
|
-
const shouldSkip =
|
|
829
|
-
(allDisabled && options?.disableTests?.[testName] !== false) ||
|
|
830
|
-
(options?.disableTests?.[testName] ?? false);
|
|
831
|
-
|
|
832
|
-
let displayName = testName.replace(
|
|
833
|
-
" - ",
|
|
834
|
-
` ${TTY_COLORS.dim}${dash}${TTY_COLORS.undim} `,
|
|
835
|
-
);
|
|
836
|
-
if (config.prefixTests) {
|
|
837
|
-
displayName = `${config.prefixTests} ${TTY_COLORS.dim}>${TTY_COLORS.undim} ${displayName}`;
|
|
838
|
-
}
|
|
839
|
-
if (helpers.prefixTests) {
|
|
840
|
-
displayName = `[${TTY_COLORS.dim}${helpers.prefixTests}${TTY_COLORS.undim}] ${displayName}`;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
test.skipIf(shouldSkip)(
|
|
844
|
-
displayName,
|
|
845
|
-
{ timeout: 30000 },
|
|
846
|
-
async ({ onTestFailed, skip }) => {
|
|
847
|
-
resetDebugLogs();
|
|
848
|
-
|
|
849
|
-
// Apply migration options before test runs
|
|
850
|
-
await (async () => {
|
|
851
|
-
if (shouldSkip) return;
|
|
852
|
-
|
|
853
|
-
const thisMigration = deepmerge(
|
|
854
|
-
config.defaultBetterAuthOptions || {},
|
|
855
|
-
migrateBetterAuth || {},
|
|
856
|
-
);
|
|
857
|
-
|
|
858
|
-
// If this is the first test in a group, migrate to the group's options
|
|
859
|
-
if (isFirstInGroup && testGroup) {
|
|
860
|
-
const groupMigrationOptions = testGroup.migrationOptions;
|
|
861
|
-
const groupFinalOptions = deepmerge(
|
|
862
|
-
config.defaultBetterAuthOptions || {},
|
|
863
|
-
groupMigrationOptions || {},
|
|
864
|
-
);
|
|
865
|
-
|
|
866
|
-
// Only migrate if the group's options are different from current state
|
|
867
|
-
if (
|
|
868
|
-
!deepEqual(
|
|
869
|
-
currentGroupMigrationOptions,
|
|
870
|
-
groupMigrationOptions,
|
|
871
|
-
)
|
|
872
|
-
) {
|
|
873
|
-
await applyOptionsAndMigrate(groupFinalOptions, true);
|
|
874
|
-
currentGroupMigrationOptions = groupMigrationOptions;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
// If this test is not in a group or not first in group, check if migration is needed
|
|
878
|
-
else if (
|
|
879
|
-
!deepEqual(currentGroupMigrationOptions, migrateBetterAuth)
|
|
880
|
-
) {
|
|
881
|
-
await applyOptionsAndMigrate(thisMigration, true);
|
|
882
|
-
currentGroupMigrationOptions = migrateBetterAuth;
|
|
883
|
-
}
|
|
884
|
-
})();
|
|
885
|
-
|
|
886
|
-
stats.testCount++;
|
|
887
|
-
|
|
888
|
-
onTestFailed(async () => {
|
|
889
|
-
printDebugLogs();
|
|
890
|
-
await onFinish(testName);
|
|
891
|
-
});
|
|
892
|
-
await testFn({ skip });
|
|
893
|
-
await onFinish(testName);
|
|
894
|
-
},
|
|
895
|
-
);
|
|
896
|
-
}
|
|
897
|
-
};
|
|
898
|
-
};
|
|
899
|
-
};
|
package/src/adapter/index.ts
DELETED
|
@@ -1,384 +0,0 @@
|
|
|
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
DELETED
package/tsdown.config.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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
|
-
sourcemap: true,
|
|
10
|
-
unbundle: true,
|
|
11
|
-
outDir: "./dist",
|
|
12
|
-
clean: true,
|
|
13
|
-
});
|