Overview

The voting system uses Vitest with Cloudflare Workers testing support to run both unit and integration tests. Tests run in a simulated Workers environment with real D1 database access.

Test Structure

Directory Layout

tests/
├── helpers/
│   ├── fixtures.ts          # Test data factories
│   ├── notion-mocks.ts      # Notion API mocks
│   └── test-db.ts           # Database test utilities
├── integration/
│   ├── auth.test.ts
│   ├── vote-flow.test.ts
│   ├── batch-invite-flow.test.ts
│   └── ...                  # End-to-end tests
├── unit/
│   ├── token.test.ts
│   ├── ballot.test.ts
│   └── ...                  # Unit tests
└── setup.ts                 # Test setup/teardown

Test Types

Unit Tests

Location: tests/unit/

Purpose: Test individual functions and modules in isolation.

Examples:

  • Token generation and hashing
  • Ballot validation
  • Email template generation
  • Notion parsing

Characteristics:

  • Fast execution
  • No external dependencies
  • Mock external services

Integration Tests

Location: tests/integration/

Purpose: Test complete workflows end-to-end.

Examples:

  • Full vote flow (create election → generate tokens → vote → view results)
  • Batch invite flow
  • Authentication flows
  • Notion integration

Characteristics:

  • Slower execution
  • Real D1 database
  • Real Workers environment
  • Test actual HTTP requests

Running Tests

All Tests

npm test

Runs all unit and integration tests.

Unit Tests Only

npm run test:unit

Runs only tests in tests/unit/.

Integration Tests Only

npm run test:integration

Runs only tests in tests/integration/.

Watch Mode

npm test -- --watch

Runs tests in watch mode, re-running on file changes.

Specific Test File

npm test tests/integration/vote-flow.test.ts

Runs a specific test file.

Test Configuration

Vitest Config

Located in vitest.config.ts:

export default defineWorkersProject({
  test: {
    pool: '@cloudflare/vitest-pool-workers',
    poolOptions: {
      workers: {
        main: './src/index.ts',
        wrangler: { configPath: './wrangler.test.jsonc' },
      },
    },
    include: ['tests/**/*.test.ts'],
    testTimeout: 30000,  // 30 second timeout
    setupFiles: ['./tests/setup.ts'],
  },
});

Test Environment

Tests run in a simulated Cloudflare Workers environment:

  • D1 Database: Real D1 database (test instance)
  • Workers Runtime: Simulated Workers runtime
  • Bindings: All bindings available (DB, TOKEN_MANAGER, etc.)

Test Database

Each test run:

  1. Creates a fresh D1 database
  2. Runs all migrations
  3. Cleans up after tests

See tests/setup.ts for database setup.

Test Helpers

Fixtures

Located in tests/helpers/fixtures.ts, provides factory functions:

createTestElection(): Creates a test election

const election = await createTestElection(env.DB, {
  title: "Test Election",
  ballot_type: "SIMPLE_TRIPLE",
  open_at: new Date(now - 3600000).toISOString(),
  close_at: new Date(now + 3600000).toISOString(),
});

createTestToken(): Creates a test token

const { token, tokenHash } = await createTestToken(env.DB, election.id);

createTestMagicLink(): Creates a test magic link

const magicLink = await createTestMagicLink(env.DB, "test@example.com");

createTestInviteWithMode(): Creates a test invite

const invite = await createTestInviteWithMode(
  env.DB,
  election.id,
  "voter@example.com",
  "batch"
);

Database Helpers

Located in tests/helpers/test-db.ts:

testNow: Provides a consistent “now” timestamp for tests

Notion Mocks

Located in tests/helpers/notion-mocks.ts:

Mocks Notion API responses for testing Notion integration without real API calls.

Writing Tests

Unit Test Example

import { describe, it, expect } from "vitest";
import { hashToken } from "../../src/models/token";
 
describe("Token Hashing", () => {
  it("should hash tokens consistently", async () => {
    const token = "test-token-123";
    const hash1 = await hashToken(token);
    const hash2 = await hashToken(token);
    
    expect(hash1).toBe(hash2);
    expect(hash1).not.toBe(token);
  });
});

Integration Test Example

import { describe, it, expect } from "vitest";
import { SELF, env } from "cloudflare:test";
import { createTestElection, createTestToken } from "../helpers/fixtures";
 
describe("Vote Flow", () => {
  it("should allow voting with valid token", async () => {
    const election = await createTestElection(env.DB, {
      ballot_type: "SIMPLE_TRIPLE",
    });
    const { token } = await createTestToken(env.DB, election.id);
    
    const response = await SELF.fetch(`http://localhost/e/${election.id}/vote`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ token, choice: "YES" }),
    });
    
    expect(response.status).toBe(200);
    const data = await response.json();
    expect(data.success).toBe(true);
  });
});

Test Structure

  1. Arrange: Set up test data
  2. Act: Execute the code under test
  3. Assert: Verify the results

Best Practices

  1. Isolation: Each test should be independent
  2. Cleanup: Clean up test data after tests
  3. Descriptive Names: Use clear test descriptions
  4. One Assertion: Prefer one assertion per test (when possible)
  5. Fast Tests: Keep tests fast (< 1 second for unit, < 5 seconds for integration)

Mocking

External Services

Notion API: Mocked in tests/helpers/notion-mocks.ts

Email Service: Mocked via custom sender functions

Example:

const mockEmailSender = async (params) => {
  sentEmails.push(params);
  return { success: true };
};
 
await sendVoteInviteEmail(params, mockEmailSender);

Durable Objects

Durable Objects are automatically available in test environment via env.TOKEN_MANAGER.

Test Coverage

Current Coverage

  • Unit Tests: Core functions (tokens, ballots, validation)
  • Integration Tests: Full workflows (voting, invites, results)
  • Edge Cases: Error handling, invalid inputs
  • Security: Token validation, authentication

Areas Needing More Tests

  • Complex Condorcet scenarios
  • High-concurrency scenarios
  • Error recovery
  • Performance under load

Continuous Integration

Running in CI

Tests should run in CI/CD pipeline:

# Example GitHub Actions
- name: Run tests
  run: npm test

Test Environment Variables

Tests use wrangler.test.jsonc for configuration:

  • Test D1 database binding
  • Test environment variables
  • No production secrets

Debugging Tests

Running Single Test

npm test -- tests/integration/vote-flow.test.ts -t "should allow voting"

Debug Output

Add console.log() statements (they appear in test output).

Test Timeout

Default timeout is 30 seconds. Increase if needed:

it("slow test", async () => {
  // ...
}, { timeout: 60000 }); // 60 seconds

Common Issues

Database Not Found

Error: “Database not found”

Solution: Ensure wrangler.test.jsonc has correct D1 binding.

Timeout Errors

Error: “Test timeout”

Solution:

  • Increase timeout
  • Check for infinite loops
  • Verify async operations complete

Flaky Tests

Causes:

  • Race conditions
  • Timing issues
  • Shared state

Solutions:

  • Use await properly
  • Isolate test data
  • Use deterministic timestamps

Performance

Test Execution Time

  • Unit Tests: < 1 second each
  • Integration Tests: 1-5 seconds each
  • Full Suite: ~30-60 seconds

Optimization

  1. Parallel Execution: Tests run in parallel when possible
  2. Fast Database: Test D1 is fast (in-memory)
  3. Minimal Setup: Only set up what’s needed

See Also