BetterAuth Testing: Plugin System, Social Providers, and Session Validation
BetterAuth is a newer authentication library for TypeScript that emphasizes type safety, a plugin system, and framework agnosticism. It's built for modern full-stack frameworks (Next.js, SvelteKit, Astro, Hono) and supports a wide range of social providers out of the box. Testing BetterAuth integrations means testing its plugin composition, session handling, and provider callbacks.
BetterAuth's Testing Model
BetterAuth exposes a testable API because its core is pure TypeScript with no framework coupling. Key testing entry points:
auth.api— the server-side API that handles authentication requestsauth.$context— the internal context (database adapter, options, plugins)- Plugin hooks — testable middleware inserted by plugins
- Social provider callbacks — OAuth callback handlers
Setting Up a Test Auth Instance
// test-utils/better-auth-setup.ts
import { betterAuth } from 'better-auth';
import { memoryAdapter } from 'better-auth/adapters/memory';
export function createTestAuth(options = {}) {
return betterAuth({
database: memoryAdapter(),
emailAndPassword: {
enabled: true,
autoSignIn: false,
},
...options,
});
}
export function createTestAuthWithPlugins(plugins: any[]) {
return betterAuth({
database: memoryAdapter(),
emailAndPassword: { enabled: true, autoSignIn: false },
plugins,
});
}Testing Email/Password Authentication
import { createTestAuth } from '../test-utils/better-auth-setup';
describe('Email/password authentication', () => {
let auth: ReturnType<typeof createTestAuth>;
beforeEach(() => {
auth = createTestAuth();
});
test('signs up a new user successfully', async () => {
const response = await auth.api.signUpEmail({
body: {
email: 'jane@example.com',
password: 'SecurePassword123!',
name: 'Jane Smith',
},
});
expect(response.user.email).toBe('jane@example.com');
expect(response.user.name).toBe('Jane Smith');
expect(response.session).toBeDefined();
expect(response.session.token).toBeTruthy();
});
test('rejects duplicate email on sign-up', async () => {
await auth.api.signUpEmail({
body: { email: 'jane@example.com', password: 'Password123!', name: 'Jane' },
});
await expect(auth.api.signUpEmail({
body: { email: 'jane@example.com', password: 'Different123!', name: 'Jane2' },
})).rejects.toThrow(/email already exists/i);
});
test('signs in with correct credentials', async () => {
await auth.api.signUpEmail({
body: { email: 'jane@example.com', password: 'Password123!', name: 'Jane' },
});
const { session, user } = await auth.api.signInEmail({
body: { email: 'jane@example.com', password: 'Password123!' },
});
expect(user.email).toBe('jane@example.com');
expect(session.token).toBeTruthy();
});
test('rejects sign-in with wrong password', async () => {
await auth.api.signUpEmail({
body: { email: 'jane@example.com', password: 'CorrectPassword123!', name: 'Jane' },
});
await expect(auth.api.signInEmail({
body: { email: 'jane@example.com', password: 'WrongPassword123!' },
})).rejects.toThrow(/invalid credentials/i);
});
});Testing Session Validation
describe('Session validation', () => {
test('validates a valid session token', async () => {
const auth = createTestAuth();
const { session } = await auth.api.signUpEmail({
body: { email: 'test@example.com', password: 'Password123!', name: 'Test' },
});
const validatedSession = await auth.api.getSession({
headers: new Headers({ Authorization: `Bearer ${session.token}` }),
});
expect(validatedSession.session.userId).toBe(session.userId);
expect(validatedSession.user.email).toBe('test@example.com');
});
test('returns null for an expired session', async () => {
const auth = createTestAuth({
session: { expiresIn: -1 }, // Expire immediately
});
const { session } = await auth.api.signUpEmail({
body: { email: 'test@example.com', password: 'Password123!', name: 'Test' },
});
const result = await auth.api.getSession({
headers: new Headers({ Authorization: `Bearer ${session.token}` }),
});
expect(result).toBeNull();
});
test('invalidates session on sign-out', async () => {
const auth = createTestAuth();
const { session } = await auth.api.signUpEmail({
body: { email: 'test@example.com', password: 'Password123!', name: 'Test' },
});
await auth.api.signOut({
headers: new Headers({ Authorization: `Bearer ${session.token}` }),
});
const result = await auth.api.getSession({
headers: new Headers({ Authorization: `Bearer ${session.token}` }),
});
expect(result).toBeNull();
});
});Testing the Plugin System
BetterAuth's plugin system is one of its strongest features. Test that plugins compose correctly:
import { twoFactor } from 'better-auth/plugins';
import { createTestAuthWithPlugins } from '../test-utils/better-auth-setup';
describe('Two-factor authentication plugin', () => {
let auth: ReturnType<typeof createTestAuthWithPlugins>;
beforeEach(() => {
auth = createTestAuthWithPlugins([
twoFactor({
issuer: 'Test App',
otpOptions: { period: 30, digits: 6 },
}),
]);
});
test('enables 2FA for a user', async () => {
const { user, session } = await auth.api.signUpEmail({
body: { email: 'jane@example.com', password: 'Password123!', name: 'Jane' },
});
const twoFactorSetup = await auth.api.enableTwoFactor({
headers: new Headers({ Authorization: `Bearer ${session.token}` }),
});
expect(twoFactorSetup.totpURI).toContain('otpauth://totp/');
expect(twoFactorSetup.backupCodes).toHaveLength(10);
});
test('requires 2FA for enabled users on sign-in', async () => {
const { session: setupSession } = await auth.api.signUpEmail({
body: { email: '2fa@example.com', password: 'Password123!', name: '2FA User' },
});
// Enable 2FA
await auth.api.enableTwoFactor({
headers: new Headers({ Authorization: `Bearer ${setupSession.token}` }),
body: { code: 'mock-totp-code' }, // In real tests, generate actual TOTP
});
// Sign in should now require 2FA
const signInResult = await auth.api.signInEmail({
body: { email: '2fa@example.com', password: 'Password123!' },
});
// With 2FA enabled, sign-in returns a twoFactorRedirect, not a full session
expect(signInResult.twoFactorRedirect).toBe(true);
});
});Testing Social Provider Configuration
Social providers require OAuth credentials. In tests, mock the OAuth exchange:
import { github } from 'better-auth/social-providers';
jest.mock('better-auth/social-providers', () => ({
github: jest.fn(() => ({
id: 'github',
name: 'GitHub',
createAuthorizationURL: jest.fn(({ state }) => ({
url: `https://github.com/login/oauth/authorize?state=${state}&client_id=test`,
codeVerifier: null,
})),
validateAuthorizationCode: jest.fn().mockResolvedValue({
accessToken: 'github-access-token',
refreshToken: null,
}),
getUserInfo: jest.fn().mockResolvedValue({
user: {
id: 'github-user-123',
email: 'github@example.com',
name: 'GitHub User',
emailVerified: true,
},
data: { login: 'githubuser', avatar_url: 'https://github.com/avatar' },
}),
})),
}));
describe('GitHub social login', () => {
test('creates authorization URL with correct state', async () => {
const auth = betterAuth({
database: memoryAdapter(),
socialProviders: {
github: github({ clientId: 'test-id', clientSecret: 'test-secret' }),
},
});
const response = await auth.api.signInSocial({
body: { provider: 'github', callbackURL: 'http://localhost:3000/callback' },
});
expect(response.url).toContain('github.com/login/oauth/authorize');
expect(response.url).toContain('state=');
});
test('handles OAuth callback and creates user session', async () => {
const auth = betterAuth({
database: memoryAdapter(),
socialProviders: {
github: github({ clientId: 'test-id', clientSecret: 'test-secret' }),
},
});
// Simulate the OAuth callback
const callbackResponse = await auth.api.callbackOAuth({
query: {
code: 'test-auth-code',
state: 'test-state',
},
params: { id: 'github' },
});
expect(callbackResponse.session).toBeDefined();
expect(callbackResponse.user.email).toBe('github@example.com');
expect(callbackResponse.user.name).toBe('GitHub User');
});
test('links social account to existing user with same email', async () => {
const auth = betterAuth({
database: memoryAdapter(),
emailAndPassword: { enabled: true, autoSignIn: false },
socialProviders: {
github: github({ clientId: 'test-id', clientSecret: 'test-secret' }),
},
});
// Create user via email
const { user: emailUser } = await auth.api.signUpEmail({
body: { email: 'github@example.com', password: 'Password123!', name: 'Email User' },
});
// Sign in via GitHub (same email)
const callbackResponse = await auth.api.callbackOAuth({
query: { code: 'test-code', state: 'state' },
params: { id: 'github' },
});
// Should link to existing account, not create a new one
expect(callbackResponse.user.id).toBe(emailUser.id);
expect(callbackResponse.user.email).toBe('github@example.com');
});
});Testing Custom Plugin Hooks
For apps that extend BetterAuth with custom plugins:
import type { BetterAuthPlugin } from 'better-auth';
function auditLogPlugin(): BetterAuthPlugin {
return {
id: 'audit-log',
hooks: {
after: [
{
matcher: (context) => context.path.startsWith('/sign-in'),
handler: async (context) => {
const userId = context.response?.user?.id;
if (userId) {
// Log the sign-in event
await logAuditEvent({ userId, action: 'sign_in', timestamp: new Date() });
}
},
},
],
},
};
}
describe('Audit log plugin', () => {
const mockLogAuditEvent = jest.fn();
beforeEach(() => {
jest.mock('../lib/audit', () => ({ logAuditEvent: mockLogAuditEvent }));
});
test('logs sign-in event for successful authentication', async () => {
const auth = createTestAuthWithPlugins([auditLogPlugin()]);
await auth.api.signUpEmail({
body: { email: 'test@example.com', password: 'Password123!', name: 'Test' },
});
await auth.api.signInEmail({
body: { email: 'test@example.com', password: 'Password123!' },
});
expect(mockLogAuditEvent).toHaveBeenCalledWith(
expect.objectContaining({
action: 'sign_in',
})
);
});
});Testing Route Protection in Next.js
import { auth } from '../lib/auth'; // Your BetterAuth instance
import { getSession } from '../lib/auth-client'; // Client-side helper
// Mock the session retrieval
jest.mock('../lib/auth-client', () => ({
getSession: jest.fn(),
}));
test('protected page redirects unauthenticated users', async () => {
(getSession as jest.Mock).mockResolvedValue(null);
const response = await fetch('http://localhost:3000/dashboard');
expect(response.redirected).toBe(true);
expect(response.url).toContain('/sign-in');
});
test('protected page renders for authenticated users', async () => {
(getSession as jest.Mock).mockResolvedValue({
session: { token: 'valid-token', userId: 'user-1' },
user: { id: 'user-1', email: 'jane@example.com', name: 'Jane' },
});
const response = await fetch('http://localhost:3000/dashboard');
expect(response.status).toBe(200);
});E2E Testing with HelpMeTest
BetterAuth's server-side session management needs browser-level testing to verify cookie behavior, OAuth redirects, and session persistence:
Navigate to https://your-app.com/sign-in
Enter email address
Enter password
Click Sign In
Verify redirect to /dashboard
Verify user menu displays name
Open a new tab to https://your-app.com/dashboard
Verify still authenticated (session cookie shared)
Click Sign Out
Verify redirect to /sign-in
Verify cannot access /dashboardHelpMeTest saves browser state (including session cookies) for reuse across tests, eliminating the need to sign in before every test.
Summary
Testing BetterAuth requires:
- Memory adapter for fast, isolated unit tests without a real database
- Email/password flow tests — sign-up, sign-in, credential validation
- Session validation tests — valid tokens, expired sessions, invalidation
- Plugin composition tests — 2FA, audit logs, and custom plugins
- Social provider tests — OAuth URL generation, callback handling, account linking
- Route protection tests — middleware redirects and API authorization
- E2E monitoring — HelpMeTest for real browser auth flow validation