import { getSchema } from 'better-auth/db' import { sql } from 'kysely' import * as TestApp from '../../test/App.js' import * as BetterAuth from './BetterAuth.js' import * as RateLimit from './RateLimit.js' describe('create', () => { test('uses the pinned schema through Tempo Kysely', async () => { const db = TestApp.database() try { const auth = BetterAuth.create(db, { basePath: '/v1/auth', baseUrl: 'https://api.example.com', rateLimit: RateLimit.memory(), secret: 'test-better-auth-secret-at-least-32-characters', secureCookies: true, }) const context = await auth.$context const user = await context.internalAdapter.createUser( { email: 'developer@example.com', name: 'Tempo Developer' }, { method: 'email-otp' }, ) expect(user).toMatchObject({ createdAt: expect.any(Date), email: 'developer@example.com', emailVerified: false, id: expect.stringMatching(/^usr_/), updatedAt: expect.any(Date), }) const account = await context.internalAdapter.createAccount({ accountId: 'developer@example.com', issuer: 'local:email-otp', providerId: 'email-otp', userId: user.id, }) const session = await context.internalAdapter.createSession(user.id) const verification = await context.internalAdapter.createVerificationValue({ expiresAt: new Date(Date.now() + 300_000), identifier: 'sign-in:developer@example.com', value: 'hashed-otp', }) await expect(context.internalAdapter.findSession(session.token)).resolves.toMatchObject({ session: { id: session.id, userId: user.id }, user: { email: 'developer@example.com', id: user.id }, }) await expect(context.internalAdapter.findAccounts(user.id)).resolves.toMatchObject([ { accountId: 'developer@example.com', id: account.id, issuer: 'local:email-otp', providerId: 'email-otp', userId: user.id, }, ]) await expect( db.kysely.selectFrom('users').selectAll().executeTakeFirst(), ).resolves.toMatchObject({ createdAt: user.createdAt.toISOString(), email: 'developer@example.com', id: user.id, updatedAt: user.updatedAt.toISOString(), }) await expect( db.kysely.selectFrom('auth_accounts').selectAll().executeTakeFirst(), ).resolves.toMatchObject({ createdAt: account.createdAt.toISOString(), updatedAt: account.updatedAt.toISOString(), }) await expect( db.kysely.selectFrom('auth_sessions').selectAll().executeTakeFirst(), ).resolves.toMatchObject({ createdAt: session.createdAt.toISOString(), expiresAt: session.expiresAt.toISOString(), updatedAt: session.updatedAt.toISOString(), }) await expect( db.kysely.selectFrom('auth_verifications').selectAll().executeTakeFirst(), ).resolves.toMatchObject({ createdAt: verification.createdAt.toISOString(), expiresAt: verification.expiresAt.toISOString(), updatedAt: verification.updatedAt.toISOString(), }) await expect( context.internalAdapter.consumeVerificationValue(verification.identifier), ).resolves.toMatchObject({ id: verification.id, value: 'hashed-otp' }) const schema = getSchema(auth.options) const tableNames = new Map([ ['authAccounts', 'auth_accounts'], ['authSessions', 'auth_sessions'], ['authVerifications', 'auth_verifications'], ['users', 'users'], ]) const expectedColumns = Object.entries(schema) .flatMap(([modelName, model]) => { const table = tableNames.get(modelName) if (!table) return [] return [ { column: 'id', nullable: false, table }, ...Object.entries(model.fields).map(([field, attributes]) => ({ column: field.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`), // Wallet-only SIWE users keep email nullable. nullable: table === 'users' && field === 'email' ? true : attributes.required === false, table, })), ] }) .sort((a, b) => `${a.table}.${a.column}`.localeCompare(`${b.table}.${b.column}`)) const currentSchema = ( await sql<{ schema: string }>`SELECT current_schema() AS schema`.execute(db.kysely) ).rows[0]!.schema const tables = await db.kysely.introspection.getTables() const actualColumns = tables .filter( (table) => table.schema === currentSchema && [...tableNames.values()].includes(table.name), ) .flatMap((table) => table.columns .filter((column) => expectedColumns.some( (expected) => expected.table === table.name && expected.column === column.name, ), ) .map((column) => ({ column: column.name, nullable: column.isNullable, table: table.name, })), ) .sort((a, b) => `${a.table}.${a.column}`.localeCompare(`${b.table}.${b.column}`)) expect(actualColumns).toStrictEqual(expectedColumns) const indexes = await sql<{ indexName: string }>` SELECT indexname AS "indexName" FROM pg_indexes WHERE schemaname = current_schema() AND tablename IN ( 'auth_accounts', 'auth_sessions', 'auth_verifications', 'users' ) ORDER BY indexname `.execute(db.kysely) expect(indexes.rows.map((row) => row.indexName)).toMatchInlineSnapshot(` [ "auth_accounts_issuer_account_id_unique_idx", "auth_accounts_pkey", "auth_accounts_user_id_idx", "auth_sessions_pkey", "auth_sessions_token_key", "auth_sessions_user_id_idx", "auth_verifications_identifier_idx", "auth_verifications_pkey", "users_address_key", "users_email_idx", "users_email_normalized_idx", "users_email_prefix_idx", "users_pkey", ] `) } finally { await db.close() } }) test('resolves a database factory once per instance', async () => { const db = TestApp.database() let calls = 0 try { BetterAuth.create( () => { calls++ return db }, { basePath: '/v1/auth', baseUrl: 'https://api.example.com', rateLimit: RateLimit.memory(), secret: 'test-better-auth-secret-at-least-32-characters', secureCookies: true, }, ) expect(calls).toBe(1) } finally { await db.close() } }) test('starts Google sign-in with the configured callback', async () => { const db = TestApp.database() try { const auth = BetterAuth.create(db, { basePath: '/v1/auth', baseUrl: 'https://console.example.com', google: { clientId: 'google-client-id', clientSecret: 'google-client-secret', }, rateLimit: RateLimit.memory(), secret: 'test-better-auth-secret-at-least-32-characters', secureCookies: true, }) const response = await auth.handler( new Request('https://console.example.com/v1/auth/sign-in/social', { body: JSON.stringify({ callbackURL: 'https://console.example.com/', provider: 'google' }), headers: { 'content-type': 'application/json', origin: 'https://console.example.com', }, method: 'POST', }), ) expect(response.status).toBe(200) const body = (await response.json()) as { redirect: boolean; url: string } const url = new URL(body.url) expect({ clientId: url.searchParams.get('client_id'), origin: url.origin, pathname: url.pathname, redirect: body.redirect, redirectUri: url.searchParams.get('redirect_uri'), scope: url.searchParams.get('scope'), }).toMatchInlineSnapshot(` { "clientId": "google-client-id", "origin": "https://accounts.google.com", "pathname": "/o/oauth2/v2/auth", "redirect": true, "redirectUri": "https://console.example.com/v1/auth/callback/google", "scope": "email profile openid", } `) } finally { await db.close() } }) })