Back to blog

Achieving 80%+ Test Coverage: Testing Strategy for Aegis2FA

Complete testing guide covering unit tests, integration tests, and E2E tests. Learn how I achieved 80%+ coverage and caught critical bugs before production.

#testing#nodejs#ci-cd#quality

Why 80% Coverage?

When building a security-focused product like Aegis2FA, bugs aren't just annoying - they're catastrophic. A single authentication bypass could compromise thousands of users.

I set a goal: 80%+ test coverage before v1.0.

Three months later:

  • 84% total coverage
  • 3 critical race conditions caught
  • Zero security vulnerabilities in audit
  • Confidence to ship

Here's how I did it.

The Testing Pyramid

I followed the testing pyramid philosophy:

      /\
     /E2E\        10% - Full user flows
    /------\
   /  INTG  \     30% - API + Database
  /----------\
 /   UNIT     \   60% - Business logic
/--------------\

Unit Tests (60%)

Fast, isolated, test business logic:

// src/services/totp.service.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { TotpService } from './totp.service';
import speakeasy from 'speakeasy';
 
describe('TotpService', () => {
  let service: TotpService;
 
  beforeEach(() => {
    service = new TotpService();
  });
 
  describe('generateSecret', () => {
    it('should generate a valid Base32 secret', () => {
      const result = service.generateSecret('user123');
 
      expect(result.secret).toMatch(/^[A-Z2-7]+=*$/);
      expect(result.secret.length).toBeGreaterThanOrEqual(32);
    });
 
    it('should include user info in otpauthUrl', () => {
      const result = service.generateSecret('user@example.com');
 
      expect(result.otpauthUrl).toContain('user@example.com');
      expect(result.otpauthUrl).toContain('Aegis2FA');
    });
  });
 
  describe('verifyToken', () => {
    it('should verify valid token', () => {
      const secret = 'JBSWY3DPEHPK3PXP';
      const token = speakeasy.totp({
        secret,
        encoding: 'base32',
      });
 
      const result = service.verifyToken(secret, token);
 
      expect(result).toBe(true);
    });
 
    it('should reject invalid token', () => {
      const secret = 'JBSWY3DPEHPK3PXP';
      const invalidToken = '000000';
 
      const result = service.verifyToken(secret, invalidToken);
 
      expect(result).toBe(false);
    });
 
    it('should reject token with wrong secret', () => {
      const secret1 = 'JBSWY3DPEHPK3PXP';
      const secret2 = 'AAAAAAAAAAAAAAAA';
 
      const token = speakeasy.totp({
        secret: secret1,
        encoding: 'base32',
      });
 
      const result = service.verifyToken(secret2, token);
 
      expect(result).toBe(false);
    });
 
    it('should accept token within time window', () => {
      const secret = 'JBSWY3DPEHPK3PXP';
 
      // Generate token from 30 seconds ago
      const pastToken = speakeasy.totp({
        secret,
        encoding: 'base32',
        time: Math.floor(Date.now() / 1000) - 30,
      });
 
      // Should still be valid with window=1
      const result = service.verifyToken(secret, pastToken, 1);
 
      expect(result).toBe(true);
    });
  });
});

Integration Tests (30%)

Test API endpoints with real database:

// tests/integration/auth.test.ts
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import request from 'supertest';
import { app } from '../../src/app';
import { db } from '../../src/db';
import { hash } from 'argon2';
 
describe('Authentication API', () => {
  beforeAll(async () => {
    await db.$connect();
  });
 
  afterAll(async () => {
    await db.$disconnect();
  });
 
  beforeEach(async () => {
    // Clean database
    await db.user.deleteMany();
    await db.refreshToken.deleteMany();
  });
 
  describe('POST /api/auth/register', () => {
    it('should register a new user', async () => {
      const res = await request(app)
        .post('/api/auth/register')
        .send({
          email: 'test@example.com',
          password: 'SecurePass123!',
          name: 'Test User',
        });
 
      expect(res.status).toBe(201);
      expect(res.body).toHaveProperty('user');
      expect(res.body.user.email).toBe('test@example.com');
      expect(res.body).toHaveProperty('accessToken');
 
      // Verify user in database
      const user = await db.user.findUnique({
        where: { email: 'test@example.com' },
      });
 
      expect(user).not.toBeNull();
      expect(user?.email).toBe('test@example.com');
    });
 
    it('should reject duplicate email', async () => {
      // Create user
      await db.user.create({
        data: {
          email: 'test@example.com',
          passwordHash: await hash('password'),
          name: 'Test User',
        },
      });
 
      // Try to register again
      const res = await request(app)
        .post('/api/auth/register')
        .send({
          email: 'test@example.com',
          password: 'SecurePass123!',
          name: 'Another User',
        });
 
      expect(res.status).toBe(409);
      expect(res.body.error).toContain('already exists');
    });
 
    it('should reject weak password', async () => {
      const res = await request(app)
        .post('/api/auth/register')
        .send({
          email: 'test@example.com',
          password: '123', // Too short
          name: 'Test User',
        });
 
      expect(res.status).toBe(400);
      expect(res.body.error).toContain('password');
    });
  });
 
  describe('POST /api/auth/login', () => {
    beforeEach(async () => {
      // Create test user
      await db.user.create({
        data: {
          email: 'test@example.com',
          passwordHash: await hash('SecurePass123!'),
          name: 'Test User',
        },
      });
    });
 
    it('should login with valid credentials', async () => {
      const res = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'test@example.com',
          password: 'SecurePass123!',
        });
 
      expect(res.status).toBe(200);
      expect(res.body).toHaveProperty('accessToken');
      expect(res.headers['set-cookie']).toBeDefined();
 
      // Verify refresh token in database
      const tokens = await db.refreshToken.findMany({
        where: { user: { email: 'test@example.com' } },
      });
 
      expect(tokens.length).toBe(1);
    });
 
    it('should reject invalid password', async () => {
      const res = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'test@example.com',
          password: 'WrongPassword',
        });
 
      expect(res.status).toBe(401);
      expect(res.body.error).toContain('Invalid credentials');
    });
 
    it('should reject non-existent user', async () => {
      const res = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'nonexistent@example.com',
          password: 'SecurePass123!',
        });
 
      expect(res.status).toBe(401);
    });
  });
 
  describe('POST /api/auth/refresh', () => {
    let refreshToken: string;
    let userId: string;
 
    beforeEach(async () => {
      // Create user and get tokens
      const user = await db.user.create({
        data: {
          email: 'test@example.com',
          passwordHash: await hash('SecurePass123!'),
          name: 'Test User',
        },
      });
      userId = user.id;
 
      const res = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'test@example.com',
          password: 'SecurePass123!',
        });
 
      const cookies = res.headers['set-cookie'] as string[];
      refreshToken = cookies[0].split(';')[0].split('=')[1];
    });
 
    it('should refresh access token', async () => {
      const res = await request(app)
        .post('/api/auth/refresh')
        .set('Cookie', `refreshToken=${refreshToken}`);
 
      expect(res.status).toBe(200);
      expect(res.body).toHaveProperty('accessToken');
 
      // Old token should be deleted
      const oldToken = await db.refreshToken.findFirst({
        where: { userId },
      });
 
      // New token should exist
      expect(oldToken).toBeDefined();
    });
 
    it('should reject invalid refresh token', async () => {
      const res = await request(app)
        .post('/api/auth/refresh')
        .set('Cookie', 'refreshToken=invalid');
 
      expect(res.status).toBe(401);
    });
  });
});

E2E Tests (10%)

Test complete user journeys:

// tests/e2e/totp-flow.test.ts
import { test, expect } from '@playwright/test';
import { db } from '../../src/db';
import { hash } from 'argon2';
 
test.describe('TOTP Authentication Flow', () => {
  test.beforeEach(async () => {
    // Clean database
    await db.user.deleteMany();
 
    // Create test user
    await db.user.create({
      data: {
        email: 'test@example.com',
        passwordHash: await hash('SecurePass123!'),
        name: 'Test User',
      },
    });
  });
 
  test('should complete full 2FA setup', async ({ page }) => {
    // 1. Login
    await page.goto('/login');
    await page.fill('input[name="email"]', 'test@example.com');
    await page.fill('input[name="password"]', 'SecurePass123!');
    await page.click('button[type="submit"]');
 
    // Wait for dashboard
    await page.waitForURL('/dashboard');
 
    // 2. Navigate to security settings
    await page.click('text=Security');
    await page.waitForURL('/settings/security');
 
    // 3. Enable 2FA
    await page.click('text=Enable Two-Factor Authentication');
 
    // 4. Verify QR code displayed
    const qrCode = page.locator('[data-testid="qr-code"]');
    await expect(qrCode).toBeVisible();
 
    // 5. Get secret for manual entry
    const secret = await page
      .locator('[data-testid="totp-secret"]')
      .textContent();
 
    // 6. Generate TOTP token (simulating authenticator app)
    const token = speakeasy.totp({
      secret: secret || '',
      encoding: 'base32',
    });
 
    // 7. Verify token
    await page.fill('input[name="token"]', token);
    await page.click('button:has-text("Verify")');
 
    // 8. Verify success message
    await expect(page.locator('text=2FA enabled successfully')).toBeVisible();
 
    // 9. Verify backup codes displayed
    const backupCodes = page.locator('[data-testid="backup-codes"]');
    await expect(backupCodes).toBeVisible();
 
    // 10. Logout
    await page.click('button:has-text("Logout")');
    await page.waitForURL('/login');
 
    // 11. Login again - should require 2FA
    await page.fill('input[name="email"]', 'test@example.com');
    await page.fill('input[name="password"]', 'SecurePass123!');
    await page.click('button[type="submit"]');
 
    // 12. Verify 2FA prompt
    await expect(page.locator('text=Enter your 2FA code')).toBeVisible();
 
    // 13. Enter TOTP token
    const newToken = speakeasy.totp({
      secret: secret || '',
      encoding: 'base32',
    });
    await page.fill('input[name="token"]', newToken);
    await page.click('button:has-text("Verify")');
 
    // 14. Verify logged in
    await page.waitForURL('/dashboard');
    await expect(page.locator('text=Welcome back')).toBeVisible();
  });
});

Test Infrastructure

Test Database Setup

// tests/setup.ts
import { PrismaClient } from '@prisma/client';
import { execSync } from 'child_process';
 
const testDb = new PrismaClient({
  datasources: {
    db: {
      url: process.env.DATABASE_URL_TEST,
    },
  },
});
 
export async function setupTestDatabase() {
  // Run migrations
  execSync('npx prisma migrate deploy', {
    env: {
      ...process.env,
      DATABASE_URL: process.env.DATABASE_URL_TEST,
    },
  });
 
  return testDb;
}
 
export async function teardownTestDatabase() {
  await testDb.$disconnect();
}

Test Fixtures

// tests/fixtures/user.fixture.ts
import { db } from '../../src/db';
import { hash } from 'argon2';
 
export async function createTestUser(overrides?: Partial<User>) {
  return db.user.create({
    data: {
      email: 'test@example.com',
      passwordHash: await hash('SecurePass123!'),
      name: 'Test User',
      ...overrides,
    },
  });
}
 
export async function createUserWith2FA() {
  const user = await createTestUser({
    totpEnabled: true,
    totpSecret: 'JBSWY3DPEHPK3PXP',
  });
 
  return {
    user,
    secret: 'JBSWY3DPEHPK3PXP',
    generateToken: () =>
      speakeasy.totp({
        secret: 'JBSWY3DPEHPK3PXP',
        encoding: 'base32',
      }),
  };
}

Mocking External Services

Email Service Mock

// tests/mocks/email.mock.ts
import { vi } from 'vitest';
 
export const mockEmailService = {
  send: vi.fn().mockResolvedValue({ messageId: 'test-123' }),
  sendVerification: vi.fn().mockResolvedValue(true),
  sendPasswordReset: vi.fn().mockResolvedValue(true),
};
 
// In tests
vi.mock('../../src/services/email.service', () => ({
  emailService: mockEmailService,
}));

SMS Service Mock

// tests/mocks/sms.mock.ts
export const mockSmsService = {
  send: vi.fn().mockResolvedValue({ sid: 'SM123', status: 'sent' }),
};
 
vi.mock('../../src/services/sms.service', () => ({
  smsService: mockSmsService,
}));

Coverage Reports

Istanbul/nyc Configuration

{
  "nyc": {
    "all": true,
    "include": ["src/**/*.ts"],
    "exclude": [
      "**/*.test.ts",
      "**/*.spec.ts",
      "tests/**",
      "**/*.d.ts"
    ],
    "reporter": ["text", "html", "lcov"],
    "check-coverage": true,
    "lines": 80,
    "functions": 80,
    "branches": 75,
    "statements": 80
  }
}

Running Tests with Coverage

# Unit tests
npm run test:unit -- --coverage
 
# Integration tests
npm run test:integration -- --coverage
 
# All tests
npm run test:all -- --coverage
 
# Generate HTML report
npm run test:coverage
open coverage/index.html

CI/CD Integration

GitHub Actions

name: Tests
 
on: [push, pull_request]
 
jobs:
  test:
    runs-on: ubuntu-latest
 
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: aegis2fa_test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
 
      redis:
        image: redis:7
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
 
    steps:
      - uses: actions/checkout@v3
 
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'
          cache: 'npm'
 
      - name: Install dependencies
        run: npm ci
 
      - name: Run migrations
        run: npx prisma migrate deploy
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/aegis2fa_test
 
      - name: Run tests with coverage
        run: npm run test:all -- --coverage
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/aegis2fa_test
          REDIS_URL: redis://localhost:6379
 
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
          fail_ci_if_error: true
 
      - name: Check coverage thresholds
        run: npm run test:coverage-check

What 80% Coverage Caught

Bug 1: Race Condition in Token Generation

// BEFORE: Race condition
async generateBackupCodes(userId: string): Promise<string[]> {
  const codes = Array.from({ length: 10 }, () =>
    crypto.randomBytes(4).toString('hex')
  );
 
  await db.backupCode.createMany({
    data: codes.map(code => ({ userId, code: await hash(code) })),
  });
 
  return codes;
}
 
// Test that caught it
it('should not generate duplicate codes on concurrent requests', async () => {
  const promises = [
    service.generateBackupCodes('user1'),
    service.generateBackupCodes('user1'),
  ];
 
  const [codes1, codes2] = await Promise.all(promises);
 
  // FAILED: Some codes were duplicated!
  expect(new Set([...codes1, ...codes2]).size).toBe(20);
});
 
// AFTER: Fixed with distributed lock
async generateBackupCodes(userId: string): Promise<string[]> {
  const lock = await this.redis.acquireLock(`backup:${userId}`, 5000);
 
  try {
    const codes = Array.from({ length: 10 }, () =>
      crypto.randomBytes(4).toString('hex')
    );
 
    await db.backupCode.createMany({
      data: codes.map(code => ({ userId, code: await hash(code) })),
    });
 
    return codes;
  } finally {
    await lock.release();
  }
}

Bug 2: Token Reuse Vulnerability

// Test that caught it
it('should reject reused TOTP token', async () => {
  const { user, generateToken } = await createUserWith2FA();
  const token = generateToken();
 
  // First use - should succeed
  const res1 = await request(app)
    .post('/api/auth/verify-2fa')
    .send({ userId: user.id, token });
 
  expect(res1.status).toBe(200);
 
  // Second use - should fail
  const res2 = await request(app)
    .post('/api/auth/verify-2fa')
    .send({ userId: user.id, token });
 
  // FAILED: Token was accepted twice!
  expect(res2.status).toBe(401);
});

Bug 3: Password Length Bypass

// Test that caught it
it('should enforce maximum password length', async () => {
  const longPassword = 'a'.repeat(10000);
 
  const res = await request(app)
    .post('/api/auth/register')
    .send({
      email: 'test@example.com',
      password: longPassword,
      name: 'Test',
    });
 
  // FAILED: Caused DoS due to Argon2 hashing time
  expect(res.status).toBe(400);
  expect(res.body.error).toContain('too long');
});

Lessons Learned

Write tests before fixing bugs: Reproduce the bug in a test first, then fix it.

Integration tests catch the most bugs: 70% of bugs I found were in integration tests.

E2E tests are expensive but valuable: Reserve them for critical user flows.

Mock external services: Don't call real email/SMS APIs in tests.

Use fixtures: Reusable test data makes tests cleaner.

Results

  • 84% coverage achieved
  • 3 critical bugs caught before production
  • Zero regressions in 6 months
  • Faster development - confidence to refactor
  • Passed security audit with flying colors

Questions about testing strategy? Need help setting up test infrastructure? Check the Aegis2FA tests folder or reach out!