Skip to main content

Running Tests

Tests are written using Vitest and can be run at the root or package level:
pnpm test
The Turborepo pipeline ensures tests run in dependency order. Tests for @ripeseed/shared run before dependent packages.

Test File Locations

API Tests

apps/api/
└── test/
    ├── unit/
    │   ├── cleanup.test.ts
    │   ├── lease.test.ts
    │   ├── quota.test.ts
    │   ├── slug.test.ts
    │   └── telemetry.test.ts
    └── integration/
        ├── auth.service.integration.test.ts
        └── tunnel.service.integration.test.ts

CLI Tests

apps/cli/
└── src/
    ├── config.test.ts
    ├── commands/
    │   └── up.test.ts
    └── lib/
        ├── local-proxy.test.ts
        └── tunnel-stats.test.ts

Shared Package Tests

packages/shared/
└── test/
    └── contracts.test.ts

Test Patterns

Unit Tests

Unit tests focus on individual functions and modules in isolation.
Example from apps/api/test/unit/slug.test.ts:
import { describe, expect, it } from 'vitest';
import { validateRequestedSlug } from '../../src/utils/slug.js';

describe('slug validation', () => {
  it('accepts a valid slug', () => {
    expect(validateRequestedSlug('abc-1')).toBe('abc-1');
  });

  it('rejects nested domains', () => {
    expect(() => validateRequestedSlug('a.b'))
      .toThrowError(/Nested domains/);
  });

  it('rejects uppercase characters', () => {
    expect(() => validateRequestedSlug('Abc'))
      .toThrowError(/Uppercase characters are not allowed/);
  });
});
Validation tests should cover both valid inputs and all error cases.

Integration Tests

Integration tests verify multiple components working together, often with external services.
// apps/api/test/integration/auth.service.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { buildApp } from '../../src/app.js';

describe('Auth Service Integration', () => {
  let app;

  beforeAll(async () => {
    app = await buildApp();
  });

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

  it('enforces email domain policy', async () => {
    const response = await app.inject({
      method: 'POST',
      url: '/v1/auth/login',
      payload: { email: '[email protected]' }
    });

    expect(response.statusCode).toBe(403);
  });
});
Integration tests for the API require a running Postgres database. Ensure Docker is running and migrations are applied before running integration tests.

Test Coverage Requirements

Expected Coverage

  • Core business logic: 80%+ coverage
  • Validation functions: 100% coverage
  • API routes: Cover happy path + error cases
  • CLI commands: Cover basic functionality + error handling

Running Coverage Reports

# Generate coverage report
pnpm --filter @ripeseed/api test --coverage

# View HTML report
open apps/api/coverage/index.html
Coverage output is stored in the coverage/ directory and cached by Turborepo.

Critical Test Areas

API Tests Must Cover

  • Valid slug formats (lowercase, hyphens, numbers)
  • Rejection of nested domains (no dots)
  • Rejection of uppercase characters
  • Slug uniqueness per user
  • Max active tunnels per user (default: 5)
  • Rejection when quota exceeded
  • Proper counting of active vs stopped tunnels
  • Email domain validation (ALLOWED_EMAIL_DOMAIN)
  • Slack workspace validation (ALLOWED_SLACK_TEAM_ID)
  • JWT token validation and expiry
  • Refresh token rotation
  • Stale lease detection (heartbeat timeout)
  • Automatic tunnel deletion
  • DNS record cleanup
  • Cloudflare resource cleanup

CLI Tests Must Cover

Local Proxy

  • HTTP request forwarding
  • WebSocket upgrades
  • Connection tracking
  • Error handling (502 responses)
  • Metrics collection

Configuration

  • Config file read/write
  • Environment variable precedence
  • Domain override persistence
  • Credential storage

Known Test Limitations

Restricted Environments: The apps/cli/src/lib/local-proxy.test.ts test suite requires permission to bind to local sockets. Tests may fail with EPERM in sandboxed or restricted environments.Workaround: Run tests in a standard local shell with appropriate network permissions.

CI Testing

Tests run automatically in GitHub Actions CI:
.github/workflows/ci.yml
name: CI

on:
  push:
    branches: ["main", "develop", "codex/**"]
  pull_request:

jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install
      - run: pnpm lint
      - run: pnpm typecheck
      - run: pnpm test
      - run: pnpm build
All checks must pass before code can be merged.

Writing New Tests

1

Create test file

Place test files next to the source code with .test.ts extension:
  • API unit tests: apps/api/test/unit/
  • API integration tests: apps/api/test/integration/
  • CLI tests: apps/cli/src/**/*.test.ts
  • Shared tests: packages/shared/test/
2

Import test utilities

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
3

Write descriptive test cases

describe('feature name', () => {
  it('should handle valid input', () => {
    // Arrange
    const input = 'valid-slug';
    
    // Act
    const result = validateSlug(input);
    
    // Assert
    expect(result).toBe('valid-slug');
  });

  it('should reject invalid input', () => {
    expect(() => validateSlug('Invalid'))
      .toThrowError(/Uppercase/);
  });
});
4

Clean up resources

Always clean up in afterAll or finally blocks:
afterAll(async () => {
  await server.close();
  await database.disconnect();
});

Best Practices

Test Isolation

  • Each test should be independent
  • Use beforeEach/afterEach for setup/teardown
  • Don’t rely on test execution order
  • Mock external dependencies

Descriptive Names

  • Use clear, descriptive test names
  • Follow “should do X when Y” pattern
  • Group related tests with describe
  • Document complex test scenarios

Error Cases

  • Test both success and failure paths
  • Verify error messages
  • Test edge cases and boundaries
  • Test timeout and retry behavior

Async Handling

  • Use async/await for async tests
  • Set appropriate timeouts
  • Handle promise rejections properly
  • Clean up async resources

Next Steps

Local Setup

Set up your development environment

Contributing

Guidelines for contributing code

Monorepo Structure

Understand the workspace layout

API Reference

Explore API endpoints to test

Build docs developers (and LLMs) love