Running Tests
Tests are written using Vitest and can be run at the root or package level:
Run all tests
Run tests for specific package
Run tests in watch mode
Run with coverage
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.
Validation Logic
Schema Validation
Network Services
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.
Example from packages/shared/test/contracts.test.ts: import { describe , expect , it } from 'vitest' ;
import {
tunnelListResponseSchema ,
tunnelTelemetryIngestRequestSchema
} from '../src/contracts.ts' ;
describe ( 'shared contracts' , () => {
it ( 'accepts tunnel list entries with lease null or object' , () => {
const parsed = tunnelListResponseSchema . parse ([
{
id: '11111111-1111-1111-1111-111111111111' ,
hostname: 'demo.tunnel.example.com' ,
slug: 'demo' ,
status: 'active' ,
requestedPort: 3000 ,
createdAt: '2026-01-01T00:00:00.000Z' ,
lease: null ,
stoppedAt: null ,
lastError: null ,
}
]);
expect ( parsed . length ). toBe ( 1 );
expect ( parsed [ 0 ]?. lease ). toBeNull ();
});
it ( 'rejects telemetry payloads with more than 200 requests' , () => {
const payload = {
region: null ,
metrics: { /* ... */ },
requests: Array . from ({ length: 201 }, () => ({ /* ... */ }))
};
expect (() => tunnelTelemetryIngestRequestSchema . parse ( payload ))
. toThrow ();
});
});
Example from apps/cli/src/lib/local-proxy.test.ts: import { describe , expect , it } from 'vitest' ;
import { startLocalProxy } from './local-proxy.js' ;
describe ( 'startLocalProxy' , () => {
it ( 'forwards HTTP requests and tracks connections' , async () => {
const requestEvents = [];
const connectionEvents = [];
const backend = await listenHttpServer (( req , res ) => {
if ( req . url === '/ok' ) {
res . writeHead ( 200 , { 'content-type' : 'text/plain' });
res . end ( 'ok' );
return ;
}
res . writeHead ( 404 );
res . end ( 'not found' );
});
const proxy = await startLocalProxy ({
targetPort: backend . port ,
onRequest : ( event ) => requestEvents . push ( event ),
onConnectionChange : ( snapshot ) => connectionEvents . push ( snapshot ),
});
try {
const response = await fetch (
`http://127.0.0.1: ${ proxy . port } /ok` ,
{ headers: { connection: 'close' } }
);
expect ( response . status ). toBe ( 200 );
expect ( await response . text ()). toBe ( 'ok' );
expect ( requestEvents . length ). toBe ( 1 );
expect ( requestEvents [ 0 ]?. statusCode ). toBe ( 200 );
} finally {
await proxy . stop ();
await closeServer ( backend . server );
}
});
});
Network tests should always clean up resources in finally blocks.
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:
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
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/
Import test utilities
import { describe , it , expect , beforeAll , afterAll } from 'vitest' ;
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/ );
});
});
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