Skip to content

[Duplicate Code] HTTP mock factory helpers duplicated across five api-proxy server test files #3939

@github-actions

Description

@github-actions

Duplicate Code Opportunity

Summary

  • Pattern: makeReq(), makeRes(), makeProxyReq(), makeProxyRes(), and getStructuredLogs() factory functions for building mock HTTP objects are defined independently in each test file.
  • Locations: 5 test files in containers/api-proxy/
  • Impact: ~55 duplicate lines across 5 files; any change to mock shape (e.g. adding a new field to res) requires editing every file independently.

Evidence

containers/api-proxy/server.anthropic-beta.test.js lines 36–85 and containers/api-proxy/server.model-not-supported.test.js lines 38–90 are nearly identical:

// Both files define the same 5 helpers, differing only in req.url:
function makeReq(headers = {}) {
  const req = new EventEmitter();
  req.url = '/v1/messages';          // anthropic-beta
  // req.url = '/v1/chat/completions'; // model-not-supported
  req.method = 'POST';
  req.headers = { 'content-type': 'application/json', ...headers };
  return req;
}

function makeRes() {
  const res = {
    headersSent: false,
    setHeader: jest.fn(),
    writeHead: jest.fn(() => { res.headersSent = true; }),
    end: jest.fn(),
    destroy: jest.fn(),
  };
  return res;
}

function makeProxyReq() {
  const proxyReq = new EventEmitter();
  proxyReq.end = jest.fn();
  proxyReq.write = jest.fn();
  proxyReq.destroy = jest.fn();
  return proxyReq;
}

function makeProxyRes(statusCode, headers = { 'content-type': 'application/json' }) {
  const proxyRes = new EventEmitter();
  proxyRes.statusCode = statusCode;
  proxyRes.headers = headers;
  proxyRes.pipe = jest.fn();
  return proxyRes;
}

function getStructuredLogs(writeSpy, eventName) {
  return writeSpy.mock.calls
    .map(([line]) => { try { return JSON.parse(line); } catch { return null; } })
    .filter(entry => entry && entry.event === eventName);
}

Simpler variants of makeReq + makeRes also appear in:

  • containers/api-proxy/server.token-guards.test.js (lines 36–52, and again at 115–132)
  • containers/api-proxy/server.proxy-headers.test.js (lines 30–51)
  • containers/api-proxy/server.error-handling.test.js (lines 31–51)

Suggested Refactoring

Extract to containers/api-proxy/test-helpers/server-mock-factories.js (the test-helpers/ directory already exists with mock-oidc-server.js and token-tracker-setup.js):

// containers/api-proxy/test-helpers/server-mock-factories.js
'use strict';
const { EventEmitter } = require('events');

function makeReq(url, headers = {}) {
  const req = new EventEmitter();
  req.url = url;
  req.method = 'POST';
  req.headers = { 'content-type': 'application/json', ...headers };
  return req;
}

function makeRes() {
  const res = {
    headersSent: false,
    setHeader: jest.fn(),
    writeHead: jest.fn(() => { res.headersSent = true; }),
    end: jest.fn(),
    destroy: jest.fn(),
  };
  return res;
}

function makeProxyReq() {
  const proxyReq = new EventEmitter();
  proxyReq.end = jest.fn();
  proxyReq.write = jest.fn();
  proxyReq.destroy = jest.fn();
  return proxyReq;
}

function makeProxyRes(statusCode, headers = { 'content-type': 'application/json' }) {
  const proxyRes = new EventEmitter();
  proxyRes.statusCode = statusCode;
  proxyRes.headers = headers;
  proxyRes.pipe = jest.fn();
  return proxyRes;
}

function getStructuredLogs(writeSpy, eventName) {
  return writeSpy.mock.calls
    .map(([line]) => { try { return JSON.parse(line); } catch { return null; } })
    .filter(entry => entry && entry.event === eventName);
}

module.exports = { makeReq, makeRes, makeProxyReq, makeProxyRes, getStructuredLogs };

Affected Files

  • containers/api-proxy/server.anthropic-beta.test.js — lines 36–85
  • containers/api-proxy/server.model-not-supported.test.js — lines 38–90
  • containers/api-proxy/server.token-guards.test.js — lines 36–52, 115–132
  • containers/api-proxy/server.proxy-headers.test.js — lines 30–51
  • containers/api-proxy/server.error-handling.test.js — lines 31–51

Effort Estimate

Low


Detected by Duplicate Code Detector workflow. Run date: 2026-05-27

Generated by Duplicate Code Detector · sonnet46 3.1M ·

  • expires on Jun 26, 2026, 10:20 PM UTC

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions