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.
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.htmlCI/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-checkWhat 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!