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:
Alex Yang
2026-03-05 13:33:10 -08:00
parent ab84a59ca9
commit 85976c2f2b
2 changed files with 106 additions and 0 deletions
@@ -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();