BetterAuth Testing: Plugin System, Social Providers, and Session Validation

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:

  1. auth.api — the server-side API that handles authentication requests
  2. auth.$context — the internal context (database adapter, options, plugins)
  3. Plugin hooks — testable middleware inserted by plugins
  4. 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 /dashboard

HelpMeTest saves browser state (including session cookies) for reuse across tests, eliminating the need to sign in before every test.

Summary

Testing BetterAuth requires:

  1. Memory adapter for fast, isolated unit tests without a real database
  2. Email/password flow tests — sign-up, sign-in, credential validation
  3. Session validation tests — valid tokens, expired sessions, invalidation
  4. Plugin composition tests — 2FA, audit logs, and custom plugins
  5. Social provider tests — OAuth URL generation, callback handling, account linking
  6. Route protection tests — middleware redirects and API authorization
  7. E2E monitoring — HelpMeTest for real browser auth flow validation

Read more