Skip to content
This repository was archived by the owner on Jan 29, 2026. It is now read-only.
This repository was archived by the owner on Jan 29, 2026. It is now read-only.

[Testing] Implement Automated API Test Suite #79

@coderabbitai

Description

@coderabbitai

🧪 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

  • 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.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).

Metadata

Metadata

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions