Claude Code Plugins

Community-maintained marketplace

Feedback

testing-best-practices

@WebDev70/hosting-google
0
0

Expert knowledge of testing Node.js and Express applications including Jest configuration, Supertest for API testing, unit vs integration vs e2e testing, mocking external APIs, test organization, code coverage, CI/CD integration, and TDD practices. Use when writing tests, setting up testing framework, debugging test failures, or adding test coverage.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name testing-best-practices
description Expert knowledge of testing Node.js and Express applications including Jest configuration, Supertest for API testing, unit vs integration vs e2e testing, mocking external APIs, test organization, code coverage, CI/CD integration, and TDD practices. Use when writing tests, setting up testing framework, debugging test failures, or adding test coverage.

Testing Best Practices

This skill provides comprehensive expert knowledge of testing Node.js/Express applications with emphasis on Jest and Supertest, test organization, mocking strategies, and achieving comprehensive test coverage.

Testing Framework Setup

Jest Installation and Configuration

Install dependencies:

npm install --save-dev jest supertest @types/jest

package.json configuration:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:verbose": "jest --verbose"
  },
  "jest": {
    "testEnvironment": "node",
    "coveragePathIgnorePatterns": [
      "/node_modules/"
    ],
    "testMatch": [
      "**/__tests__/**/*.js",
      "**/?(*.)+(spec|test).js"
    ]
  }
}

jest.config.js (advanced):

module.exports = {
  // Use Node.js test environment
  testEnvironment: 'node',

  // Test file patterns
  testMatch: [
    '**/__tests__/**/*.js',
    '**/*.test.js',
    '**/*.spec.js'
  ],

  // Coverage settings
  collectCoverageFrom: [
    'src/**/*.js',
    'routes/**/*.js',
    '!src/index.js', // Exclude entry point
    '!**/node_modules/**'
  ],

  // Coverage thresholds
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },

  // Setup files
  setupFilesAfterEnv: ['<rootDir>/test/setup.js'],

  // Clear mocks between tests
  clearMocks: true,

  // Verbose output
  verbose: true,

  // Timeout for tests
  testTimeout: 10000
};

Test Directory Structure

Option 1: Separate test directory:

project/
├── src/
│   ├── server.js
│   ├── routes/
│   │   └── api.js
│   └── utils/
│       └── validators.js
├── test/
│   ├── setup.js
│   ├── server.test.js
│   ├── routes/
│   │   └── api.test.js
│   └── utils/
│       └── validators.test.js
└── package.json

Option 2: Co-located tests:

project/
├── src/
│   ├── server.js
│   ├── server.test.js
│   ├── routes/
│   │   ├── api.js
│   │   └── api.test.js
│   └── utils/
│       ├── validators.js
│       └── validators.test.js
└── package.json

Option 3: tests directories:

project/
├── src/
│   ├── __tests__/
│   │   └── server.test.js
│   ├── server.js
│   ├── routes/
│   │   ├── __tests__/
│   │   │   └── api.test.js
│   │   └── api.js
└── package.json

Testing Express Applications with Supertest

Basic API Testing

const request = require('supertest');
const app = require('../server');

describe('GET /', () => {
  it('should return 200 status', async () => {
    const response = await request(app).get('/');
    expect(response.status).toBe(200);
  });

  it('should return JSON content type', async () => {
    const response = await request(app).get('/api/users');
    expect(response.headers['content-type']).toMatch(/json/);
  });

  it('should return users array', async () => {
    const response = await request(app).get('/api/users');
    expect(response.body).toHaveProperty('users');
    expect(Array.isArray(response.body.users)).toBe(true);
  });
});

describe('POST /api/users', () => {
  it('should create a user with valid data', async () => {
    const userData = {
      name: 'John Doe',
      email: 'john@example.com',
      password: 'SecurePass123!'
    };

    const response = await request(app)
      .post('/api/users')
      .send(userData)
      .set('Content-Type', 'application/json')
      .expect(201);

    expect(response.body).toHaveProperty('id');
    expect(response.body.email).toBe(userData.email);
    expect(response.body).not.toHaveProperty('password'); // Don't return password
  });

  it('should reject invalid email', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        name: 'John',
        email: 'invalid-email',
        password: 'SecurePass123!'
      })
      .expect(400);

    expect(response.body).toHaveProperty('error');
    expect(response.body.error).toMatch(/email/i);
  });

  it('should reject weak password', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        name: 'John',
        email: 'john@example.com',
        password: '123' // Too short
      })
      .expect(400);

    expect(response.body).toHaveProperty('error');
    expect(response.body.error).toMatch(/password/i);
  });
});

describe('Authentication', () => {
  let authToken;

  beforeAll(async () => {
    // Create a test user and get token
    const response = await request(app)
      .post('/api/login')
      .send({
        email: 'test@example.com',
        password: 'TestPass123!'
      });

    authToken = response.body.token;
  });

  it('should access protected route with valid token', async () => {
    const response = await request(app)
      .get('/api/profile')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    expect(response.body).toHaveProperty('user');
  });

  it('should reject access without token', async () => {
    await request(app)
      .get('/api/profile')
      .expect(401);
  });

  it('should reject invalid token', async () => {
    await request(app)
      .get('/api/profile')
      .set('Authorization', 'Bearer invalid-token')
      .expect(401);
  });
});

Testing Proxy Endpoints

const request = require('supertest');
const axios = require('axios');
const app = require('../server');

// Mock axios
jest.mock('axios');

describe('POST /api/proxy', () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  it('should proxy request successfully', async () => {
    const mockData = {
      results: [
        { id: 1, name: 'Result 1' },
        { id: 2, name: 'Result 2' }
      ]
    };

    axios.post.mockResolvedValue({
      data: mockData,
      status: 200
    });

    const response = await request(app)
      .post('/api/proxy')
      .send({ query: 'test' })
      .expect(200);

    expect(response.body).toEqual(mockData);
    expect(axios.post).toHaveBeenCalledWith(
      expect.any(String),
      { query: 'test' },
      expect.any(Object)
    );
  });

  it('should handle proxy errors', async () => {
    axios.post.mockRejectedValue({
      response: {
        status: 500,
        data: { error: 'Internal Server Error' }
      }
    });

    const response = await request(app)
      .post('/api/proxy')
      .send({ query: 'test' })
      .expect(500);

    expect(response.body).toHaveProperty('error');
  });

  it('should handle network errors', async () => {
    axios.post.mockRejectedValue(new Error('Network error'));

    const response = await request(app)
      .post('/api/proxy')
      .send({ query: 'test' })
      .expect(500);

    expect(response.body).toHaveProperty('error');
  });

  it('should validate request before proxying', async () => {
    const response = await request(app)
      .post('/api/proxy')
      .send({ invalid: 'data' })
      .expect(400);

    expect(response.body).toHaveProperty('error');
    expect(axios.post).not.toHaveBeenCalled();
  });
});

Mocking Strategies

Mocking External APIs

Mock entire module:

jest.mock('axios');

const axios = require('axios');

describe('External API calls', () => {
  it('should fetch data from external API', async () => {
    const mockData = { data: 'test' };
    axios.get.mockResolvedValue({ data: mockData });

    const result = await fetchExternalData();

    expect(result).toEqual(mockData);
    expect(axios.get).toHaveBeenCalledWith('https://api.example.com/data');
  });
});

Mock specific functions:

const userService = require('../services/user');

jest.spyOn(userService, 'findById').mockResolvedValue({
  id: 1,
  name: 'Test User'
});

describe('User routes', () => {
  it('should get user by id', async () => {
    const response = await request(app)
      .get('/api/users/1')
      .expect(200);

    expect(response.body.name).toBe('Test User');
    expect(userService.findById).toHaveBeenCalledWith('1');
  });
});

Manual mocks:

// __mocks__/axios.js
module.exports = {
  get: jest.fn(() => Promise.resolve({ data: {} })),
  post: jest.fn(() => Promise.resolve({ data: {} })),
  put: jest.fn(() => Promise.resolve({ data: {} })),
  delete: jest.fn(() => Promise.resolve({ data: {} }))
};

Mocking Database

// Mock database module
jest.mock('../db');

const db = require('../db');

describe('Database operations', () => {
  beforeEach(() => {
    db.query.mockClear();
  });

  it('should query users', async () => {
    const mockUsers = [
      { id: 1, name: 'User 1' },
      { id: 2, name: 'User 2' }
    ];

    db.query.mockResolvedValue({ rows: mockUsers });

    const users = await User.findAll();

    expect(users).toEqual(mockUsers);
    expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
  });

  it('should handle database errors', async () => {
    db.query.mockRejectedValue(new Error('Connection failed'));

    await expect(User.findAll()).rejects.toThrow('Connection failed');
  });
});

Mocking Environment Variables

describe('Environment configuration', () => {
  const originalEnv = process.env;

  beforeEach(() => {
    jest.resetModules();
    process.env = { ...originalEnv };
  });

  afterAll(() => {
    process.env = originalEnv;
  });

  it('should use default port when PORT not set', () => {
    delete process.env.PORT;
    const config = require('../config');
    expect(config.port).toBe(3000);
  });

  it('should use PORT from environment', () => {
    process.env.PORT = '8080';
    const config = require('../config');
    expect(config.port).toBe(8080);
  });
});

Unit vs Integration vs E2E Testing

Unit Tests

What: Test individual functions/modules in isolation

Example:

// validators.js
function isValidEmail(email) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

function isStrongPassword(password) {
  return password.length >= 12 &&
    /[A-Z]/.test(password) &&
    /[a-z]/.test(password) &&
    /[0-9]/.test(password) &&
    /[^A-Za-z0-9]/.test(password);
}

module.exports = { isValidEmail, isStrongPassword };

// validators.test.js
const { isValidEmail, isStrongPassword } = require('./validators');

describe('Email validation', () => {
  it('should accept valid email', () => {
    expect(isValidEmail('test@example.com')).toBe(true);
  });

  it('should reject email without @', () => {
    expect(isValidEmail('testexample.com')).toBe(false);
  });

  it('should reject email without domain', () => {
    expect(isValidEmail('test@')).toBe(false);
  });

  it('should reject email with spaces', () => {
    expect(isValidEmail('test @example.com')).toBe(false);
  });
});

describe('Password validation', () => {
  it('should accept strong password', () => {
    expect(isStrongPassword('MyP@ssw0rd123!')).toBe(true);
  });

  it('should reject short password', () => {
    expect(isStrongPassword('Short1!')).toBe(false);
  });

  it('should reject password without uppercase', () => {
    expect(isStrongPassword('myp@ssw0rd123!')).toBe(false);
  });

  it('should reject password without special char', () => {
    expect(isStrongPassword('MyPassword123')).toBe(false);
  });
});

Integration Tests

What: Test multiple components working together

Example:

const request = require('supertest');
const app = require('../server');
const db = require('../db');

describe('User registration flow', () => {
  beforeEach(async () => {
    // Clean database before each test
    await db.query('DELETE FROM users');
  });

  it('should register user and allow login', async () => {
    // Register user
    const registerResponse = await request(app)
      .post('/api/register')
      .send({
        email: 'test@example.com',
        password: 'SecurePass123!',
        name: 'Test User'
      })
      .expect(201);

    expect(registerResponse.body).toHaveProperty('id');

    // Login with registered credentials
    const loginResponse = await request(app)
      .post('/api/login')
      .send({
        email: 'test@example.com',
        password: 'SecurePass123!'
      })
      .expect(200);

    expect(loginResponse.body).toHaveProperty('token');

    // Access protected route with token
    const profileResponse = await request(app)
      .get('/api/profile')
      .set('Authorization', `Bearer ${loginResponse.body.token}`)
      .expect(200);

    expect(profileResponse.body.email).toBe('test@example.com');
  });
});

End-to-End (E2E) Tests

What: Test complete user workflows from UI to database

Setup with Puppeteer:

npm install --save-dev puppeteer

Example:

const puppeteer = require('puppeteer');

describe('E2E: User registration', () => {
  let browser;
  let page;

  beforeAll(async () => {
    browser = await puppeteer.launch({
      headless: true,
      args: ['--no-sandbox']
    });
    page = await browser.newPage();
  });

  afterAll(async () => {
    await browser.close();
  });

  it('should complete registration flow', async () => {
    // Navigate to registration page
    await page.goto('http://localhost:3000/register');

    // Fill out form
    await page.type('#email', 'test@example.com');
    await page.type('#password', 'SecurePass123!');
    await page.type('#confirmPassword', 'SecurePass123!');

    // Submit form
    await page.click('button[type="submit"]');

    // Wait for redirect to dashboard
    await page.waitForNavigation();

    // Verify we're on dashboard
    const url = page.url();
    expect(url).toContain('/dashboard');

    // Verify welcome message
    const welcomeMessage = await page.$eval(
      '.welcome',
      el => el.textContent
    );
    expect(welcomeMessage).toContain('test@example.com');
  });
});

Test Organization

Describe Blocks

describe('User API', () => {
  describe('GET /api/users', () => {
    it('should return all users', async () => {
      // Test implementation
    });

    it('should support pagination', async () => {
      // Test implementation
    });

    it('should support filtering', async () => {
      // Test implementation
    });
  });

  describe('POST /api/users', () => {
    it('should create user with valid data', async () => {
      // Test implementation
    });

    it('should reject duplicate email', async () => {
      // Test implementation
    });
  });

  describe('PUT /api/users/:id', () => {
    it('should update user', async () => {
      // Test implementation
    });

    it('should reject unauthorized update', async () => {
      // Test implementation
    });
  });
});

Setup and Teardown

describe('Database tests', () => {
  // Runs once before all tests in this describe block
  beforeAll(async () => {
    await db.connect();
  });

  // Runs once after all tests in this describe block
  afterAll(async () => {
    await db.disconnect();
  });

  // Runs before each test in this describe block
  beforeEach(async () => {
    await db.query('DELETE FROM users');
    await db.query('INSERT INTO users (email) VALUES ($1)', ['test@example.com']);
  });

  // Runs after each test in this describe block
  afterEach(async () => {
    jest.clearAllMocks();
  });

  it('should find user', async () => {
    const user = await User.findByEmail('test@example.com');
    expect(user).toBeTruthy();
  });

  it('should delete user', async () => {
    await User.deleteByEmail('test@example.com');
    const user = await User.findByEmail('test@example.com');
    expect(user).toBeNull();
  });
});

Test Fixtures

// test/fixtures/users.js
module.exports = {
  validUser: {
    email: 'test@example.com',
    password: 'SecurePass123!',
    name: 'Test User'
  },

  adminUser: {
    email: 'admin@example.com',
    password: 'AdminPass123!',
    name: 'Admin User',
    role: 'admin'
  },

  invalidUsers: {
    noEmail: {
      password: 'SecurePass123!',
      name: 'Test User'
    },
    weakPassword: {
      email: 'test@example.com',
      password: '123',
      name: 'Test User'
    }
  }
};

// Usage in tests
const fixtures = require('./fixtures/users');

describe('User creation', () => {
  it('should create valid user', async () => {
    const response = await request(app)
      .post('/api/users')
      .send(fixtures.validUser)
      .expect(201);
  });

  it('should reject user without email', async () => {
    const response = await request(app)
      .post('/api/users')
      .send(fixtures.invalidUsers.noEmail)
      .expect(400);
  });
});

Async Testing

Testing Promises

describe('Async operations', () => {
  it('should resolve with data', async () => {
    const data = await fetchData();
    expect(data).toBeDefined();
  });

  it('should reject with error', async () => {
    await expect(fetchInvalidData()).rejects.toThrow('Not found');
  });

  // Alternative: using done callback
  it('should fetch data (callback style)', (done) => {
    fetchData()
      .then(data => {
        expect(data).toBeDefined();
        done();
      })
      .catch(done);
  });
});

Testing Callbacks

describe('Callback functions', () => {
  it('should call callback with data', (done) => {
    fetchDataWithCallback((err, data) => {
      expect(err).toBeNull();
      expect(data).toBeDefined();
      done();
    });
  });

  it('should call callback with error', (done) => {
    fetchInvalidDataWithCallback((err, data) => {
      expect(err).toBeTruthy();
      expect(data).toBeUndefined();
      done();
    });
  });
});

Code Coverage

Generating Coverage Reports

# Run tests with coverage
npm run test:coverage

# Coverage report output
----------|---------|----------|---------|---------|
File      | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
All files |   85.5  |   78.3   |   91.2  |   85.1  |
 server.js|   92.3  |   85.7   |   100   |   91.8  |
 routes/  |   78.9  |   71.4   |   83.3  |   79.2  |
----------|---------|----------|---------|---------|

Coverage Configuration

// jest.config.js
module.exports = {
  collectCoverageFrom: [
    'src/**/*.js',
    '!src/index.js', // Exclude entry point
    '!src/**/*.test.js', // Exclude test files
    '!src/**/__tests__/**' // Exclude test directories
  ],

  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    },
    // Per-file thresholds
    './src/critical-module.js': {
      branches: 100,
      functions: 100,
      lines: 100,
      statements: 100
    }
  },

  coverageReporters: [
    'text',      // Terminal output
    'html',      // HTML report in coverage/
    'lcov',      // For CI tools
    'json'       // Machine-readable
  ]
};

Viewing HTML Coverage Report

npm run test:coverage
open coverage/index.html  # macOS
xdg-open coverage/index.html  # Linux
start coverage/index.html  # Windows

Testing Best Practices

1. Naming Conventions

// GOOD - Descriptive test names
describe('User registration', () => {
  it('should create user with valid email and password', () => {});
  it('should reject registration with duplicate email', () => {});
  it('should hash password before storing', () => {});
});

// BAD - Vague test names
describe('User', () => {
  it('works', () => {});
  it('test 1', () => {});
  it('should not fail', () => {});
});

2. AAA Pattern (Arrange, Act, Assert)

it('should calculate total price with tax', () => {
  // Arrange: Set up test data
  const items = [
    { price: 10, quantity: 2 },
    { price: 5, quantity: 3 }
  ];
  const taxRate = 0.1;

  // Act: Perform the action
  const total = calculateTotal(items, taxRate);

  // Assert: Verify the result
  expect(total).toBe(38.5); // (10*2 + 5*3) * 1.1
});

3. Test One Thing

// GOOD - Each test checks one behavior
it('should validate email format', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
});

it('should reject email without domain', () => {
  expect(isValidEmail('test@')).toBe(false);
});

// BAD - Testing multiple things
it('should validate inputs', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
  expect(isValidPassword('pass123')).toBe(false);
  expect(isValidPhone('1234567890')).toBe(true);
});

4. Avoid Test Interdependence

// BAD - Tests depend on each other
let userId;

it('should create user', async () => {
  const response = await createUser();
  userId = response.id; // Other tests depend on this
});

it('should update user', async () => {
  await updateUser(userId); // Fails if previous test fails
});

// GOOD - Each test is independent
describe('User operations', () => {
  let userId;

  beforeEach(async () => {
    const user = await createUser();
    userId = user.id;
  });

  it('should update user', async () => {
    await updateUser(userId);
  });

  it('should delete user', async () => {
    await deleteUser(userId);
  });
});

5. Use Meaningful Assertions

// GOOD - Specific assertions
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('users');
expect(response.body.users).toHaveLength(5);
expect(response.body.users[0]).toMatchObject({
  id: expect.any(Number),
  email: expect.stringMatching(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/)
});

// BAD - Vague assertions
expect(response).toBeTruthy();
expect(response.body).toBeDefined();

6. Test Edge Cases

describe('Division function', () => {
  it('should divide positive numbers', () => {
    expect(divide(10, 2)).toBe(5);
  });

  it('should handle negative numbers', () => {
    expect(divide(-10, 2)).toBe(-5);
  });

  it('should handle zero numerator', () => {
    expect(divide(0, 5)).toBe(0);
  });

  it('should throw error for division by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });

  it('should handle decimal results', () => {
    expect(divide(5, 2)).toBe(2.5);
  });
});

CI/CD Integration

GitHub Actions

# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18.x, 20.x]

    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}

    - name: Install dependencies
      run: npm ci

    - name: Run tests
      run: npm test

    - name: Generate coverage report
      run: npm run test:coverage

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/coverage-final.json
        fail_ci_if_error: true

npm Scripts for CI

{
  "scripts": {
    "test": "jest",
    "test:ci": "jest --ci --coverage --maxWorkers=2",
    "test:coverage": "jest --coverage",
    "test:watch": "jest --watch"
  }
}

Common Jest Matchers

Equality

expect(value).toBe(4); // Strict equality (===)
expect(value).toEqual({ a: 1 }); // Deep equality
expect(value).not.toBe(5); // Negation

Truthiness

expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();

Numbers

expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
expect(value).toBeCloseTo(0.3); // Floating point

Strings

expect(string).toMatch(/pattern/);
expect(string).toMatch('substring');
expect(string).toContain('substring');

Arrays and Iterables

expect(array).toContain('item');
expect(array).toHaveLength(3);
expect(array).toEqual(expect.arrayContaining([1, 2]));

Objects

expect(object).toHaveProperty('key');
expect(object).toHaveProperty('key', value);
expect(object).toMatchObject({ a: 1, b: 2 });
expect(object).toEqual(expect.objectContaining({ a: 1 }));

Functions

expect(fn).toThrow();
expect(fn).toThrow('error message');
expect(fn).toThrow(Error);
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledWith(arg1, arg2);
expect(fn).toHaveBeenCalledTimes(3);

Testing Checklist

Unit Tests

  • Test pure functions in isolation
  • Test all code paths (happy path and error cases)
  • Test edge cases and boundary conditions
  • Mock external dependencies
  • Achieve high code coverage (>80%)

Integration Tests

  • Test API endpoints
  • Test authentication/authorization
  • Test database operations
  • Test external API integration
  • Test error handling

E2E Tests

  • Test critical user flows
  • Test form submissions
  • Test navigation
  • Test authentication flow

General

  • Tests are fast (< 5 seconds for unit tests)
  • Tests are independent (can run in any order)
  • Tests are repeatable (same result every time)
  • Tests have clear, descriptive names
  • Setup and teardown properly implemented
  • No hardcoded values (use constants/fixtures)
  • CI/CD integration configured

Example Test Suite for Express API

const request = require('supertest');
const app = require('../server');
const db = require('../db');

describe('Express API Tests', () => {
  // Setup: Connect to test database
  beforeAll(async () => {
    await db.connect(process.env.TEST_DATABASE_URL);
  });

  // Cleanup: Disconnect from database
  afterAll(async () => {
    await db.disconnect();
  });

  // Reset database before each test
  beforeEach(async () => {
    await db.query('DELETE FROM users');
  });

  describe('GET /api/health', () => {
    it('should return health status', async () => {
      const response = await request(app)
        .get('/api/health')
        .expect(200);

      expect(response.body).toEqual({
        status: 'ok',
        timestamp: expect.any(Number)
      });
    });
  });

  describe('POST /api/users', () => {
    it('should create user with valid data', async () => {
      const userData = {
        email: 'test@example.com',
        password: 'SecurePass123!',
        name: 'Test User'
      };

      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(201);

      expect(response.body).toMatchObject({
        id: expect.any(Number),
        email: userData.email,
        name: userData.name
      });
      expect(response.body).not.toHaveProperty('password');
    });

    it('should reject duplicate email', async () => {
      const userData = {
        email: 'test@example.com',
        password: 'SecurePass123!',
        name: 'Test User'
      };

      // Create first user
      await request(app).post('/api/users').send(userData);

      // Try to create duplicate
      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(409);

      expect(response.body.error).toMatch(/already exists/i);
    });

    it('should validate email format', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({
          email: 'invalid-email',
          password: 'SecurePass123!',
          name: 'Test'
        })
        .expect(400);

      expect(response.body.error).toMatch(/email/i);
    });

    it('should enforce password requirements', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({
          email: 'test@example.com',
          password: 'weak',
          name: 'Test'
        })
        .expect(400);

      expect(response.body.error).toMatch(/password/i);
    });
  });

  describe('Authentication', () => {
    let authToken;
    const testUser = {
      email: 'auth@example.com',
      password: 'SecurePass123!',
      name: 'Auth User'
    };

    beforeEach(async () => {
      // Create user
      await request(app).post('/api/users').send(testUser);

      // Login and get token
      const response = await request(app)
        .post('/api/login')
        .send({
          email: testUser.email,
          password: testUser.password
        });

      authToken = response.body.token;
    });

    it('should login with valid credentials', async () => {
      const response = await request(app)
        .post('/api/login')
        .send({
          email: testUser.email,
          password: testUser.password
        })
        .expect(200);

      expect(response.body).toHaveProperty('token');
    });

    it('should reject invalid credentials', async () => {
      await request(app)
        .post('/api/login')
        .send({
          email: testUser.email,
          password: 'WrongPassword'
        })
        .expect(401);
    });

    it('should access protected route with token', async () => {
      const response = await request(app)
        .get('/api/profile')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);

      expect(response.body.email).toBe(testUser.email);
    });

    it('should reject access without token', async () => {
      await request(app)
        .get('/api/profile')
        .expect(401);
    });
  });
});

Resources