mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 11:24:57 -05:00
test: add regression tests for #8156 (secondaryStorage with auto-deserialized JSON)
Add tests covering the scenario where secondaryStorage (e.g. @vercel/kv) returns already-parsed objects instead of strings, causing expiresAt to be a string rather than a Date. The root cause was fixed in #8248 via reviveDates in safeJSONParse. These tests prevent regression.
This commit is contained in:
@@ -366,6 +366,50 @@ describe("updateUser", async () => {
|
||||
expect(firstSession?.user.name).toBe("updatedName");
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://github.com/better-auth/better-auth/issues/8156
|
||||
*/
|
||||
it("should propagate updates when secondaryStorage auto-deserializes JSON (e.g. @vercel/kv)", async () => {
|
||||
const store = new Map<string, unknown>();
|
||||
const { client: authClient, signInWithTestUser: signIn } =
|
||||
await getTestInstance({
|
||||
secondaryStorage: {
|
||||
set(key, value, ttl) {
|
||||
// Simulate @vercel/kv behavior: stores string but get() returns parsed object
|
||||
store.set(key, JSON.parse(value));
|
||||
},
|
||||
get(key) {
|
||||
return store.get(key) ?? null;
|
||||
},
|
||||
delete(key) {
|
||||
store.delete(key);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { headers: headers1 } = await signIn();
|
||||
|
||||
// Verify the store actually holds a parsed object with string dates
|
||||
const session1 = await authClient.getSession({
|
||||
fetchOptions: { headers: headers1, throw: true },
|
||||
});
|
||||
const cached = store.get(session1!.session.token) as
|
||||
| { session?: { expiresAt?: unknown } }
|
||||
| undefined;
|
||||
expect(typeof cached?.session?.expiresAt).toBe("string");
|
||||
|
||||
// This was the crash in #8156: TypeError: expiresAt.getTime is not a function
|
||||
await authClient.updateUser({
|
||||
name: "updatedViaKV",
|
||||
fetchOptions: { headers: headers1, throw: true },
|
||||
});
|
||||
|
||||
const refreshed = await authClient.getSession({
|
||||
fetchOptions: { headers: headers1, throw: true },
|
||||
});
|
||||
expect(refreshed?.user.name).toBe("updatedViaKV");
|
||||
});
|
||||
|
||||
it("should not write to secondary storage multiple times for the same session token during updateUser", async () => {
|
||||
const store = new Map<string, string>();
|
||||
const writeLog: { key: string; timestamp: number }[] = [];
|
||||
|
||||
@@ -602,6 +602,68 @@ describe("internal adapter test", async () => {
|
||||
expect(finalTTL).toBeGreaterThanOrEqual(7198); // Allow for test execution time
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://github.com/better-auth/better-auth/issues/8156
|
||||
*/
|
||||
it("should handle secondaryStorage that auto-deserializes JSON (e.g. @vercel/kv)", async () => {
|
||||
const objectStore = new Map<string, unknown>();
|
||||
|
||||
const testOpts = {
|
||||
database: new DatabaseSync(":memory:"),
|
||||
secondaryStorage: {
|
||||
set(key: string, value: string, _ttl?: number) {
|
||||
// Simulate @vercel/kv: stores string but get() returns parsed object
|
||||
objectStore.set(key, JSON.parse(value));
|
||||
},
|
||||
get(key: string) {
|
||||
return objectStore.get(key) ?? null;
|
||||
},
|
||||
delete(key: string) {
|
||||
objectStore.delete(key);
|
||||
},
|
||||
},
|
||||
} satisfies BetterAuthOptions;
|
||||
|
||||
(await getMigrations(testOpts)).runMigrations();
|
||||
const testCtx = await init(testOpts);
|
||||
const testAdapter = testCtx.internalAdapter;
|
||||
|
||||
const user = await testAdapter.createUser({
|
||||
name: "KV User",
|
||||
email: `kv-test-${Date.now()}@example.com`,
|
||||
});
|
||||
const session = await testAdapter.createSession(user.id);
|
||||
|
||||
// Verify store holds parsed objects with string dates (not Date instances)
|
||||
const cachedBefore = objectStore.get(session.token) as
|
||||
| { session?: { expiresAt?: unknown } }
|
||||
| undefined;
|
||||
expect(typeof cachedBefore?.session?.expiresAt).toBe("string");
|
||||
|
||||
// This was the crash in #8156: TypeError: expiresAt.getTime is not a function
|
||||
// updateUser calls refreshUserSessions which reads from secondary storage
|
||||
await expect(
|
||||
testAdapter.updateUser(user.id, { name: "Updated Name" }),
|
||||
).resolves.toBeDefined();
|
||||
|
||||
const afterUpdate = objectStore.get(session.token) as
|
||||
| { user?: { name?: string } }
|
||||
| undefined;
|
||||
expect(afterUpdate?.user?.name).toBe("Updated Name");
|
||||
|
||||
// Also verify updateUserByEmail works
|
||||
await expect(
|
||||
testAdapter.updateUserByEmail(user.email, {
|
||||
name: "Updated By Email",
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
|
||||
const afterEmailUpdate = objectStore.get(session.token) as
|
||||
| { user?: { name?: string } }
|
||||
| undefined;
|
||||
expect(afterEmailUpdate?.user?.name).toBe("Updated By Email");
|
||||
});
|
||||
|
||||
it("should create on secondary storage", async () => {
|
||||
// Create session
|
||||
const now = Date.now();
|
||||
|
||||
Reference in New Issue
Block a user