diff --git a/packages/better-auth/src/api/routes/update-user.test.ts b/packages/better-auth/src/api/routes/update-user.test.ts index 97edb070f2..720895c459 100644 --- a/packages/better-auth/src/api/routes/update-user.test.ts +++ b/packages/better-auth/src/api/routes/update-user.test.ts @@ -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(); + 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(); const writeLog: { key: string; timestamp: number }[] = []; diff --git a/packages/better-auth/src/db/internal-adapter.test.ts b/packages/better-auth/src/db/internal-adapter.test.ts index be9b148ba8..6cb587a233 100644 --- a/packages/better-auth/src/db/internal-adapter.test.ts +++ b/packages/better-auth/src/db/internal-adapter.test.ts @@ -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(); + + 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();