🧪 Priority: LOW - Nice to Have
Background
The new backend API infrastructure introduced in PR #66 lacks automated tests. While the code is well-structured with comprehensive JSDoc comments, automated tests are essential for catching regressions, validating behavior, and enabling confident refactoring.
Current State - No Test Coverage
No test files exist for:
- API controllers (
WorkflowController.js, StoreController.js)
- API services (
WorkflowService.js, StoreService.js)
- Middleware (
auth.js, rateLimit.js, validation.js, errorHandler.js)
- Database layer (
database.js)
- WebSocket server (
websocket/server.js)
Recommended Solution
Part 1: Test Infrastructure Setup
npm install --save-dev jest supertest @types/jest @types/supertest
// package.json
{
"scripts": {
"test": "NODE_ENV=test jest",
"test:watch": "NODE_ENV=test jest --watch",
"test:coverage": "NODE_ENV=test jest --coverage"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "coverage",
"collectCoverageFrom": [
"backend/src/**/*.js",
"!backend/src/**/*.test.js"
],
"testMatch": [
"**/__tests__/**/*.js",
"**/*.test.js"
],
"setupFilesAfterEnv": ["<rootDir>/backend/test/setup.js"]
}
}
Part 2: Test Setup and Utilities
// backend/test/setup.js
import fs from 'fs/promises';
import path from 'path';
const TEST_DATA_DIR = path.join(process.cwd(), '.data-test');
// Setup test environment
beforeAll(async () => {
process.env.NODE_ENV = 'test';
process.env.API_KEY = 'test-api-key-for-automated-tests';
// Create test data directory
await fs.mkdir(TEST_DATA_DIR, { recursive: true });
});
// Cleanup after all tests
afterAll(async () => {
// Clean up test data
await fs.rm(TEST_DATA_DIR, { recursive: true, force: true });
});
// Clear data between tests
beforeEach(async () => {
// Clear test database files
const files = ['workflows.json', 'store-state.json', 'sessions.json'];
for (const file of files) {
const filePath = path.join(TEST_DATA_DIR, file);
try {
await fs.unlink(filePath);
} catch {
// File might not exist, ignore
}
}
});
// backend/test/helpers.js
import request from 'supertest';
export const API_KEY = 'test-api-key-for-automated-tests';
export function createAuthenticatedRequest(app) {
return {
get: (url) => request(app).get(url).set('X-API-Key', API_KEY),
post: (url) => request(app).post(url).set('X-API-Key', API_KEY),
put: (url) => request(app).put(url).set('X-API-Key', API_KEY),
delete: (url) => request(app).delete(url).set('X-API-Key', API_KEY)
};
}
export function createTestWorkflow(overrides = {}) {
return {
metadata: {
id: `test-${Date.now()}`,
name: 'Test Workflow',
description: 'Test workflow for automated tests',
version: '1.0.0',
...overrides.metadata
},
nodes: overrides.nodes || [
{ id: 'node1', type: 'input', data: { label: 'Input' } }
],
edges: overrides.edges || [],
...overrides
};
}
Part 3: Workflow API Tests
// backend/src/api/routes/__tests__/workflows.test.js
import request from 'supertest';
import app from '../../../server.js';
import * as db from '../../../db/database.js';
import { createAuthenticatedRequest, createTestWorkflow } from '../../../../test/helpers.js';
describe('Workflow API', () => {
let api;
beforeAll(async () => {
await db.initialize();
api = createAuthenticatedRequest(app);
});
describe('POST /api/workflows', () => {
it('should create a new workflow', async () => {
const workflow = createTestWorkflow();
const res = await api.post('/api/workflows').send(workflow).expect(201);
expect(res.body.success).toBe(true);
expect(res.body.data.metadata.id).toBeDefined();
expect(res.body.data.metadata.name).toBe('Test Workflow');
});
it('should reject workflow without authentication', async () => {
const workflow = createTestWorkflow();
await request(app)
.post('/api/workflows')
.send(workflow)
.expect(401);
});
it('should reject invalid workflow data', async () => {
const invalidWorkflow = { name: 'Invalid' }; // Missing required fields
const res = await api.post('/api/workflows').send(invalidWorkflow).expect(400);
expect(res.body.error).toBeDefined();
});
});
describe('GET /api/workflows', () => {
beforeEach(async () => {
// Create test workflows
await api.post('/api/workflows').send(createTestWorkflow({ metadata: { name: 'Workflow 1' } }));
await api.post('/api/workflows').send(createTestWorkflow({ metadata: { name: 'Workflow 2' } }));
});
it('should list all workflows', async () => {
const res = await api.get('/api/workflows').expect(200);
expect(res.body.success).toBe(true);
expect(res.body.data.workflows.length).toBeGreaterThanOrEqual(2);
expect(res.body.data.total).toBeGreaterThanOrEqual(2);
});
it('should filter workflows by tag', async () => {
await api.post('/api/workflows').send(
createTestWorkflow({ metadata: { name: 'Tagged', tags: ['production'] } })
);
const res = await api.get('/api/workflows?tags=production').expect(200);
expect(res.body.data.workflows.every(w =>
w.metadata.tags?.includes('production')
)).toBe(true);
});
it('should paginate results', async () => {
const res = await api.get('/api/workflows?limit=1&offset=0').expect(200);
expect(res.body.data.workflows.length).toBe(1);
});
});
describe('GET /api/workflows/:id', () => {
let workflowId;
beforeEach(async () => {
const res = await api.post('/api/workflows').send(createTestWorkflow());
workflowId = res.body.data.metadata.id;
});
it('should retrieve a workflow by ID', async () => {
const res = await api.get(`/api/workflows/${workflowId}`).expect(200);
expect(res.body.success).toBe(true);
expect(res.body.data.metadata.id).toBe(workflowId);
});
it('should return 404 for non-existent workflow', async () => {
await api.get('/api/workflows/non-existent-id').expect(404);
});
});
describe('PUT /api/workflows/:id', () => {
let workflowId;
beforeEach(async () => {
const res = await api.post('/api/workflows').send(createTestWorkflow());
workflowId = res.body.data.metadata.id;
});
it('should update a workflow', async () => {
const updates = {
metadata: { name: 'Updated Workflow', description: 'Updated description' }
};
const res = await api.put(`/api/workflows/${workflowId}`).send(updates).expect(200);
expect(res.body.data.metadata.name).toBe('Updated Workflow');
expect(res.body.data.metadata.description).toBe('Updated description');
});
it('should return 404 when updating non-existent workflow', async () => {
await api.put('/api/workflows/non-existent-id').send({}).expect(404);
});
});
describe('DELETE /api/workflows/:id', () => {
let workflowId;
beforeEach(async () => {
const res = await api.post('/api/workflows').send(createTestWorkflow());
workflowId = res.body.data.metadata.id;
});
it('should delete a workflow', async () => {
await api.delete(`/api/workflows/${workflowId}`).expect(200);
// Verify it's deleted
await api.get(`/api/workflows/${workflowId}`).expect(404);
});
it('should return 404 when deleting non-existent workflow', async () => {
await api.delete('/api/workflows/non-existent-id').expect(404);
});
});
});
Part 4: Middleware Tests
// backend/src/api/middleware/__tests__/auth.test.js
import { authenticate } from '../auth.js';
describe('Authentication Middleware', () => {
let req, res, next;
beforeEach(() => {
req = { headers: {} };
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
next = jest.fn();
});
it('should allow requests with valid API key', () => {
req.headers['x-api-key'] = process.env.API_KEY;
const middleware = authenticate({ required: true });
middleware(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
it('should reject requests without API key', () => {
const middleware = authenticate({ required: true });
middleware(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(next).not.toHaveBeenCalled();
});
it('should reject requests with invalid API key', () => {
req.headers['x-api-key'] = 'invalid-key';
const middleware = authenticate({ required: true });
middleware(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(next).not.toHaveBeenCalled();
});
it('should allow unauthenticated requests when optional', () => {
const middleware = authenticate({ required: false });
middleware(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
});
// backend/src/api/middleware/__tests__/rateLimit.test.js
import request from 'supertest';
import app from '../../../server.js';
describe('Rate Limiting Middleware', () => {
it('should allow requests within rate limit', async () => {
for (let i = 0; i < 10; i++) {
await request(app).get('/health').expect(200);
}
});
it('should block requests exceeding rate limit', async () => {
// Make MAX_REQUESTS_PER_WINDOW requests
for (let i = 0; i < 100; i++) {
await request(app).get('/health');
}
// Next request should be rate limited
const res = await request(app).get('/health').expect(429);
expect(res.body.error.message).toContain('Too many requests');
});
it('should include rate limit headers', async () => {
const res = await request(app).get('/health').expect(200);
expect(res.headers['x-ratelimit-limit']).toBeDefined();
expect(res.headers['x-ratelimit-remaining']).toBeDefined();
expect(res.headers['x-ratelimit-reset']).toBeDefined();
});
});
Part 5: Database Tests
// backend/src/db/__tests__/database.test.js
import * as db from '../database.js';
describe('Database Layer', () => {
beforeAll(async () => {
await db.initialize();
});
describe('Workflow Operations', () => {
it('should create and retrieve a workflow', async () => {
const workflow = {
metadata: { id: 'test-1', name: 'Test' },
nodes: [],
edges: []
};
await db.createWorkflow(workflow);
const retrieved = await db.getWorkflowById('test-1');
expect(retrieved.metadata.id).toBe('test-1');
expect(retrieved.metadata.name).toBe('Test');
});
it('should update a workflow', async () => {
const workflow = {
metadata: { id: 'test-2', name: 'Original' },
nodes: [],
edges: []
};
await db.createWorkflow(workflow);
await db.updateWorkflow('test-2', { metadata: { name: 'Updated' } });
const updated = await db.getWorkflowById('test-2');
expect(updated.metadata.name).toBe('Updated');
});
it('should delete a workflow', async () => {
const workflow = {
metadata: { id: 'test-3', name: 'To Delete' },
nodes: [],
edges: []
};
await db.createWorkflow(workflow);
await db.deleteWorkflow('test-3');
const deleted = await db.getWorkflowById('test-3');
expect(deleted).toBeNull();
});
});
});
Part 6: WebSocket Tests
// backend/src/websocket/__tests__/server.test.js
import WebSocket from 'ws';
import { WebSocketServer } from '../server.js';
describe('WebSocket Server', () => {
let wsServer;
let httpServer;
beforeAll(() => {
httpServer = { on: jest.fn() };
wsServer = new WebSocketServer(httpServer);
});
it('should accept client connections', (done) => {
const client = new WebSocket('ws://localhost:3001/ws');
client.on('open', () => {
expect(client.readyState).toBe(WebSocket.OPEN);
client.close();
done();
});
});
it('should broadcast events to all clients', (done) => {
const client1 = new WebSocket('ws://localhost:3001/ws');
const client2 = new WebSocket('ws://localhost:3001/ws');
let receivedCount = 0;
const handleMessage = () => {
receivedCount++;
if (receivedCount === 2) {
client1.close();
client2.close();
done();
}
};
client1.on('message', handleMessage);
client2.on('message', handleMessage);
// Wait for both to connect, then broadcast
setTimeout(() => {
wsServer.broadcast({ type: 'test', data: { message: 'hello' } });
}, 100);
});
});
Files to Create
backend/test/setup.js (test configuration)
backend/test/helpers.js (test utilities)
backend/src/api/routes/__tests__/workflows.test.js
backend/src/api/routes/__tests__/store.test.js
backend/src/api/middleware/__tests__/auth.test.js
backend/src/api/middleware/__tests__/rateLimit.test.js
backend/src/api/middleware/__tests__/validation.test.js
backend/src/api/services/__tests__/WorkflowService.test.js
backend/src/api/services/__tests__/StoreService.test.js
backend/src/db/__tests__/database.test.js
backend/src/websocket/__tests__/server.test.js
Files to Modify
package.json (add test scripts and Jest configuration)
.gitignore (add coverage/ and .data-test/)
Test Coverage Goals
- Controllers: 80%+ coverage
- Services: 90%+ coverage
- Middleware: 95%+ coverage
- Database: 90%+ coverage
Acceptance Criteria
Running Tests
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run with coverage report
npm run test:coverage
# Run specific test file
npm test -- workflows.test.js
# Run tests matching pattern
npm test -- --testNamePattern="should create"
CI/CD Integration (GitHub Actions)
# .github/workflows/test.yml
name: Backend Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: cd backend && npm ci
- name: Run tests
run: cd backend && npm test
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./backend/coverage/lcov.info
References
Additional Context
Automated tests provide confidence when refactoring, prevent regressions, and serve as documentation for API behavior. Implement tests incrementally, starting with critical paths (workflow CRUD, authentication).
🧪 Priority: LOW - Nice to Have
Background
The new backend API infrastructure introduced in PR #66 lacks automated tests. While the code is well-structured with comprehensive JSDoc comments, automated tests are essential for catching regressions, validating behavior, and enabling confident refactoring.
Current State - No Test Coverage
No test files exist for:
WorkflowController.js,StoreController.js)WorkflowService.js,StoreService.js)auth.js,rateLimit.js,validation.js,errorHandler.js)database.js)websocket/server.js)Recommended Solution
Part 1: Test Infrastructure Setup
Part 2: Test Setup and Utilities
Part 3: Workflow API Tests
Part 4: Middleware Tests
Part 5: Database Tests
Part 6: WebSocket Tests
Files to Create
backend/test/setup.js(test configuration)backend/test/helpers.js(test utilities)backend/src/api/routes/__tests__/workflows.test.jsbackend/src/api/routes/__tests__/store.test.jsbackend/src/api/middleware/__tests__/auth.test.jsbackend/src/api/middleware/__tests__/rateLimit.test.jsbackend/src/api/middleware/__tests__/validation.test.jsbackend/src/api/services/__tests__/WorkflowService.test.jsbackend/src/api/services/__tests__/StoreService.test.jsbackend/src/db/__tests__/database.test.jsbackend/src/websocket/__tests__/server.test.jsFiles to Modify
package.json(add test scripts and Jest configuration).gitignore(add coverage/ and .data-test/)Test Coverage Goals
Acceptance Criteria
npm testnpm run test:coverageRunning Tests
CI/CD Integration (GitHub Actions)
References
Additional Context
Automated tests provide confidence when refactoring, prevent regressions, and serve as documentation for API behavior. Implement tests incrementally, starting with critical paths (workflow CRUD, authentication).