@open-kingdom/shared-backend-integration-test-doubles
v0.0.2-18
Published
A library for creating integration test doubles (fakes) for external services. Provides local twins that can be used in place of real services during testing and development.
Readme
Integration Test Doubles
A library for creating integration test doubles (fakes) for external services. Provides local twins that can be used in place of real services during testing and development.
Features
- GCS Twin: Local fake for Google Cloud Storage using
fsouza/fake-gcs-server - Configuration: Environment-based configuration with sensible defaults and validation
- Lifecycle Management: Start, stop, and reset twins programmatically
- NestJS Integration: REST API controller for managing twins at runtime
Quick Start
Using GCS Twin
import { GcsTwin } from '@open-kingdom/shared-backend-integration-test-doubles';
// Start the twin (default port 9013)
const twin = new GcsTwin();
await twin.start();
// GCS_EMULATOR_URL is automatically set to http://localhost:9013
// Use the twin URL in your SDK configuration
const client = new Storage({
apiEndpoint: twin.getEmulatorHost(),
projectId: 'emulator-project',
credentials: {
client_email: '[email protected]',
private_key: 'unused',
},
});
// Run your tests...
// Clean up (also unsets GCS_EMULATOR_URL)
await twin.stop();Using YouTube Twin (Server-Side Interception)
import { YoutubeTwin, NodeInterceptor, RoutingTable, defaultRoutingEntries } from '@open-kingdom/shared-backend-integration-test-doubles';
// Start the twin (default port 9016)
const twin = new YoutubeTwin();
await twin.start();
// Install the global fetch interceptor
const routingTable = new RoutingTable(defaultRoutingEntries);
const interceptor = new NodeInterceptor(routingTable);
interceptor.install();
// Call the production YouTube API endpoint (internally routed to twin)
const res = await fetch('https://www.googleapis.com/youtube/v3/search?q=yoga&key=valid-key');
const data = await res.json();
console.log(data.kind); // "youtube#searchListResponse"
// Clean up
interceptor.uninstall();
await twin.stop();NestJS Integration
Use the TwinService to manage the twin lifecycle within a NestJS application:
import { TwinService } from './twin/twin.service';
// The service wraps GcsTwin and exposes start/stop/status methods.
// See the demo-scaffold-backend app for a full controller example.
const twinService = new TwinService();
await twinService.start();
const status = await twinService.status();
await twinService.stop();API Reference
GcsTwin
Constructor
constructor(overrides?: Partial<GcsTwinConfig>, docker?: Docker)overrides.port: Port to run the twin on (default: 9013, or fromGCS_TWIN_PORTenv var; must be within 9010-9020)overrides.externalUrl: External URL for the twin (default:http://localhost:{port})overrides.dataDir: Optional persistent data directoryoverrides.buckets: Buckets to create and seed (default:[{ name: 'app-assets' }, { name: 'user-uploads' }])docker: Optional Docker instance (for testing)
Config merges defaults, environment variables, and explicit overrides (explicit overrides take precedence).
Methods
start(): Start the twin container, create buckets, and setGCS_EMULATOR_URLenv varstop(): Stop and remove the container, unsetGCS_EMULATOR_URLreset(): Reset the twin to its initial seeded stategetEmulatorHost(): Get the twin URL (e.g.,http://localhost:9013)isHealthy(): Check if the twin is running and responding
GcsTwinConfig
interface GcsTwinConfig {
port: number; // Port the fake-gcs-server listens on
externalUrl: string; // External URL for SDK configuration (e.g., http://localhost:9013)
dataDir?: string; // Optional persistent data directory
buckets: BucketConfig[]; // Buckets to create on startup
}
interface BucketConfig {
name: string; // Bucket name
}YouTubeTwin & Network Interceptor
YouTubeTwin Constructor
constructor(overrides?: Partial<YoutubeTwinConfig>)overrides.port: Port to run the twin on (default: 9016, or fromYOUTUBE_TWIN_PORTenv var; must be within 9010-9020)overrides.externalUrl: External URL for the twin (default:http://localhost:{port})
YouTubeTwin Methods
start(): Start the Express serverstop(): Stop the Express serverreset(): Reset search fixtures to defaults and clear any active error modegetEmulatorHost(): Get the twin URL (e.g.,http://localhost:9016)isHealthy(): Check if the twin is running and responding
NodeInterceptor & RoutingTable
new NodeInterceptor(routingTable): Instantiate interceptorinstall(): PatchesglobalThis.fetchto intercept matching domainsuninstall(): RestoresglobalThis.fetchto its original referenceisActive(): Check if interceptor is currently active
Browser-Side MSW Interception
Use getYoutubeMswHandlerConfigs(twinBaseUrl) to export mock route definitions for browser-side testing frameworks like MSW:
import { http, HttpResponse } from 'msw';
import { getYoutubeMswHandlerConfigs } from '@open-kingdom/shared-backend-integration-test-doubles';
const configs = getYoutubeMswHandlerConfigs('http://localhost:9016');
const handlers = configs.map((config) =>
http.get(config.url, async () => {
const res = await fetch(config.proxyTo);
const body = await res.text();
return new HttpResponse(body, {
headers: { 'Content-Type': config.contentType },
});
})
);Exports
// GCS twin
export { GcsTwin } from './gcs/gcs-twin.js';
export type { GcsTwinConfig, BucketConfig } from './gcs/gcs-twin.config.js';
export { defaultGcsConfig } from './gcs/gcs-twin.config.js';
// Shared constants and utilities
export { PORT_RANGE, DEFAULT_PORTS, ENV_VARS } from './shared/constants.js';
export { isTestMode } from './shared/activation.js';
export { createGcsConfig, defaultGcsConfig } from './shared/config.js';Configuration
Environment Variables
| Variable | Description | Default |
| ------------------- | ------------------------------------- | ------------------------- |
| GCS_TWIN_PORT | Port for GCS twin (must be 9010-9020) | 9013 |
| GCS_TWIN_DATA_DIR | Persistent data directory (optional) | (ephemeral) |
| GCS_EMULATOR_URL | Auto-set by GcsTwin.start() | http://localhost:{port} |
| TEST_MODE | Activation gate; must be 'true' | (unset) |
Port Assignments
The library reserves ports 9010-9020 for test doubles:
| Twin | Default Port | | --------------- | ------------- | | GCS | 9013 | | Gmail | 9014 (future) | | Google Auth | 9015 (future) | | YouTube | 9016 | | Google Calendar | 9017 (future) | | Spotify | 9018 (future) |
Configure in constants.ts if you need to change this range.
How Configuration Merging Works
Configuration is resolved in this order (later takes precedence):
- Defaults (
defaultGcsConfig) - Environment variables (
GCS_TWIN_PORT,GCS_TWIN_DATA_DIR) - Explicit overrides (passed to constructor or
createGcsConfig())
Port validation ensures the value is within the reserved range (9010-9020).
Scripts
Start Twins Manually
# Start GCS twin manually on default port (9013)
npx tsx scripts/start-twin.ts
# Start YouTube twin manually on default port (9016)
npx nx run integration-test-doubles:start:youtube-twinThe script outputs the twin URL and sets GCS_EMULATOR_URL. Keeps running until Ctrl+C.
Architecture
Components
┌─────────────────┐ ┌────────────────────┐
│ NestJS App │ │ GcsTwin │
│ (GcsStorage │────▶│ (fake-gcs-server │
│ Service) │ │ Docker container) │
└─────────────────┘ └────────────────────┘How It Works
- GcsTwin starts a Docker container running
fsouza/fake-gcs-server GCS_EMULATOR_URLis automatically set inprocess.envon start and unset on stop- GcsStorageService (in
feature-gcp-resources) readsGCS_EMULATOR_URLorSTORAGE_EMULATOR_HOSTand configures the Google Cloud Storage SDK withapiEndpointpointing to the twin - When no emulator is configured,
GcsStorageServicefalls back to real GCS with signed URLs
Default Buckets
Two buckets are created by default:
- app-assets - Application static assets
- user-uploads - User-uploaded files
Troubleshooting
Docker Not Running
Error: connect ENOENT /var/run/docker.sockSolution: Ensure Docker Desktop is running.
Port Already in Use
Error: Port 9013 is already in useSolution:
- Use a different port:
new GcsTwin({ port: 9014 }) - Or set
GCS_TWIN_PORT=9014
Container Won't Start
Error: (HTTP code 404) not found - No such image: fsouza/fake-gcs-server:latestSolution: Pull the image manually:
docker pull fsouza/fake-gcs-serverPort Out of Range
Error: GCS twin port 9099 is outside the reserved range 9010-9020Solution: Use a port within 9010-9020, or adjust PORT_RANGE in constants.ts.
Tests Fail When Run Together
Symptoms: Tests pass in isolation but fail in suite.
Cause: Port conflicts between tests.
Solution: Use unique ports for each test suite:
const twin1 = new GcsTwin({ port: 9013 });
const twin2 = new GcsTwin({ port: 9014 });License
MIT