This repository was archived by the owner on Jan 29, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 70
This repository was archived by the owner on Jan 29, 2026. It is now read-only.
[Testing] Implement Automated API Test Suite #79
Copy link
Copy link
Open
Labels
codexOpenAI's CodexOpenAI's CodexdocumentationImprovements or additions to documentationImprovements or additions to documentationenhancementNew feature or requestNew feature or requestgen/qol improvements
Description
🧪 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.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.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
- Jest and supertest installed and configured
- Test setup and teardown utilities created
- API route tests cover all CRUD operations
- Middleware tests verify authentication, rate limiting, validation
- Service layer tests verify business logic
- Database tests verify CRUD operations
- WebSocket tests verify connection and broadcasting
- All tests pass with
npm test - Coverage report generated with
npm run test:coverage - CI/CD pipeline configured to run tests on PR
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.infoReferences
- Pull Request: [PDE-3] Refactor: adding TUI & other upgrades #66
- Review Comment: [PDE-3] Refactor: adding TUI & other upgrades #66
- Jest Documentation: https://jestjs.io/
- Supertest: https://github.com/visionmedia/supertest
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).
Reactions are currently unavailable
Metadata
Metadata
Labels
codexOpenAI's CodexOpenAI's CodexdocumentationImprovements or additions to documentationImprovements or additions to documentationenhancementNew feature or requestNew feature or requestgen/qol improvements