Skip to content

Commit 7c9791a

Browse files
authored
refactor: split api-proxy-service.test.ts into focused test modules (#3299)
* Initial plan * refactor: split api-proxy-service.test.ts into focused modules Split 1,195-line monolithic test into four focused files + shared test-utils: - api-proxy-service-config.test.ts service config, image, healthcheck, limits - api-proxy-service-key-isolation.test.ts security: key non-leakage (ANTHROPIC/OPENAI/GEMINI) - api-proxy-service-rate-limit.test.ts rate limiting, token guard, max-runs, timeout - api-proxy-service-env-forwarding.test.ts OIDC, Anthropic, Copilot, Gemini env forwarding - api-proxy-service.test-utils.ts shared mockNetworkConfigWithProxy All 114 tests pass across 4 new files (same count as original). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 43bd596 commit 7c9791a

5 files changed

Lines changed: 591 additions & 510 deletions
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import { generateDockerCompose } from '../compose-generator';
2+
import { WrapperConfig } from '../types';
3+
import { baseConfig, mockNetworkConfig, useTempWorkDir } from '../test-helpers/docker-test-fixtures.test-utils';
4+
import { mockNetworkConfigWithProxy } from './api-proxy-service.test-utils';
5+
6+
// Create mock functions (must remain per-file — jest.mock() is hoisted before imports)
7+
8+
// Mock execa module
9+
// eslint-disable-next-line @typescript-eslint/no-require-imports
10+
jest.mock('execa', () => require('../test-helpers/mock-execa.test-utils').execaMockFactory());
11+
12+
let mockConfig: WrapperConfig;
13+
14+
describe('API proxy sidecar: service configuration', () => {
15+
useTempWorkDir(
16+
baseConfig,
17+
(config) => {
18+
mockConfig = config;
19+
},
20+
() => mockConfig
21+
);
22+
23+
it('should not include api-proxy service when enableApiProxy is false', () => {
24+
const result = generateDockerCompose(mockConfig, mockNetworkConfigWithProxy);
25+
expect(result.services['api-proxy']).toBeUndefined();
26+
});
27+
28+
it('should not include api-proxy service when enableApiProxy is true but no proxyIp', () => {
29+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' };
30+
const result = generateDockerCompose(configWithProxy, mockNetworkConfig);
31+
expect(result.services['api-proxy']).toBeUndefined();
32+
});
33+
34+
it('should include api-proxy service when enableApiProxy is true with OpenAI key', () => {
35+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-openai-key' };
36+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
37+
expect(result.services['api-proxy']).toBeDefined();
38+
const proxy = result.services['api-proxy'];
39+
expect(proxy.container_name).toBe('awf-api-proxy');
40+
expect((proxy.networks as any)['awf-net'].ipv4_address).toBe('172.30.0.30');
41+
});
42+
43+
it('should include api-proxy service when enableApiProxy is true with Anthropic key', () => {
44+
const configWithProxy = { ...mockConfig, enableApiProxy: true, anthropicApiKey: 'sk-ant-test-key' };
45+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
46+
expect(result.services['api-proxy']).toBeDefined();
47+
const proxy = result.services['api-proxy'];
48+
expect(proxy.container_name).toBe('awf-api-proxy');
49+
});
50+
51+
it('should include api-proxy service with both keys', () => {
52+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-openai-key', anthropicApiKey: 'sk-ant-test-key' };
53+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
54+
expect(result.services['api-proxy']).toBeDefined();
55+
const proxy = result.services['api-proxy'];
56+
const env = proxy.environment as Record<string, string>;
57+
expect(env.OPENAI_API_KEY).toBe('sk-test-openai-key');
58+
expect(env.ANTHROPIC_API_KEY).toBe('sk-ant-test-key');
59+
});
60+
61+
it('should only pass OpenAI key when only OpenAI key is provided', () => {
62+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-openai-key' };
63+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
64+
const proxy = result.services['api-proxy'];
65+
const env = proxy.environment as Record<string, string>;
66+
expect(env.OPENAI_API_KEY).toBe('sk-test-openai-key');
67+
expect(env.ANTHROPIC_API_KEY).toBeUndefined();
68+
});
69+
70+
it('should only pass Anthropic key when only Anthropic key is provided', () => {
71+
const configWithProxy = { ...mockConfig, enableApiProxy: true, anthropicApiKey: 'sk-ant-test-key' };
72+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
73+
const proxy = result.services['api-proxy'];
74+
const env = proxy.environment as Record<string, string>;
75+
expect(env.ANTHROPIC_API_KEY).toBe('sk-ant-test-key');
76+
expect(env.OPENAI_API_KEY).toBeUndefined();
77+
});
78+
79+
it('should use GHCR image by default', () => {
80+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key', buildLocal: false };
81+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
82+
const proxy = result.services['api-proxy'];
83+
expect(proxy.image).toBe('ghcr.io/github/gh-aw-firewall/api-proxy:latest');
84+
expect(proxy.build).toBeUndefined();
85+
});
86+
87+
it('should build locally when buildLocal is true', () => {
88+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key', buildLocal: true };
89+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
90+
const proxy = result.services['api-proxy'];
91+
expect(proxy.build).toBeDefined();
92+
expect((proxy.build as any).context).toContain('containers/api-proxy');
93+
expect(proxy.image).toBeUndefined();
94+
});
95+
96+
it('should use custom registry and tag', () => {
97+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key', buildLocal: false, imageRegistry: 'my-registry.com', imageTag: 'v1.0.0' };
98+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
99+
const proxy = result.services['api-proxy'];
100+
expect(proxy.image).toBe('my-registry.com/api-proxy:v1.0.0');
101+
});
102+
103+
it('should configure healthcheck for api-proxy', () => {
104+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' };
105+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
106+
const proxy = result.services['api-proxy'];
107+
expect(proxy.healthcheck).toBeDefined();
108+
const healthcheck = proxy.healthcheck!;
109+
expect(healthcheck.test).toEqual(['CMD', 'curl', '-f', 'http://localhost:10000/health']);
110+
expect(healthcheck.timeout).toBe('3s');
111+
expect(healthcheck.retries).toBe(15);
112+
expect(healthcheck.start_period).toBe('30s');
113+
});
114+
115+
it('should drop all capabilities', () => {
116+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' };
117+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
118+
const proxy = result.services['api-proxy'];
119+
expect(proxy.cap_drop).toEqual(['ALL']);
120+
expect(proxy.security_opt).toContain('no-new-privileges:true');
121+
});
122+
123+
it('should set stop_grace_period on api-proxy service', () => {
124+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' };
125+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
126+
const proxy = result.services['api-proxy'] as any;
127+
expect(proxy.stop_grace_period).toBe('2s');
128+
});
129+
130+
it('should set resource limits', () => {
131+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' };
132+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
133+
const proxy = result.services['api-proxy'];
134+
expect(proxy.mem_limit).toBe('512m');
135+
expect(proxy.memswap_limit).toBe('512m');
136+
expect(proxy.pids_limit).toBe(100);
137+
expect(proxy.cpu_shares).toBe(512);
138+
});
139+
140+
it('should update agent depends_on to wait for api-proxy', () => {
141+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' };
142+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
143+
const agent = result.services.agent;
144+
const dependsOn = agent.depends_on as { [key: string]: { condition: string } };
145+
expect(dependsOn['api-proxy']).toBeDefined();
146+
expect(dependsOn['api-proxy'].condition).toBe('service_healthy');
147+
});
148+
149+
it('should set OPENAI_BASE_URL in agent when OpenAI key is provided', () => {
150+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' };
151+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
152+
const agent = result.services.agent;
153+
const env = agent.environment as Record<string, string>;
154+
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000');
155+
});
156+
157+
it('should configure HTTP_PROXY and HTTPS_PROXY in api-proxy to route through Squid', () => {
158+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' };
159+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
160+
const proxy = result.services['api-proxy'];
161+
const env = proxy.environment as Record<string, string>;
162+
expect(env.HTTP_PROXY).toBe('http://172.30.0.10:3128');
163+
expect(env.HTTPS_PROXY).toBe('http://172.30.0.10:3128');
164+
});
165+
166+
it('should set ANTHROPIC_BASE_URL in agent when Anthropic key is provided', () => {
167+
const configWithProxy = { ...mockConfig, enableApiProxy: true, anthropicApiKey: 'sk-ant-test-key' };
168+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
169+
const agent = result.services.agent;
170+
const env = agent.environment as Record<string, string>;
171+
expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001');
172+
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation');
173+
expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh');
174+
});
175+
176+
it('should set both ANTHROPIC_BASE_URL and OPENAI_BASE_URL when both keys are provided', () => {
177+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-openai-key', anthropicApiKey: 'sk-ant-test-key' };
178+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
179+
const agent = result.services.agent;
180+
const env = agent.environment as Record<string, string>;
181+
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000');
182+
expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001');
183+
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation');
184+
expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh');
185+
});
186+
187+
it('should not set OPENAI_BASE_URL in agent when only Anthropic key is provided', () => {
188+
const configWithProxy = { ...mockConfig, enableApiProxy: true, anthropicApiKey: 'sk-ant-test-key' };
189+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
190+
const agent = result.services.agent;
191+
const env = agent.environment as Record<string, string>;
192+
expect(env.OPENAI_BASE_URL).toBeUndefined();
193+
expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001');
194+
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation');
195+
expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh');
196+
});
197+
198+
it('should set OPENAI_BASE_URL and not set ANTHROPIC_BASE_URL when only OpenAI key is provided', () => {
199+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' };
200+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
201+
const agent = result.services.agent;
202+
const env = agent.environment as Record<string, string>;
203+
expect(env.ANTHROPIC_BASE_URL).toBeUndefined();
204+
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000');
205+
});
206+
207+
it('should set AWF_API_PROXY_IP in agent environment', () => {
208+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' };
209+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
210+
const agent = result.services.agent;
211+
const env = agent.environment as Record<string, string>;
212+
expect(env.AWF_API_PROXY_IP).toBe('172.30.0.30');
213+
});
214+
215+
it('should set NO_PROXY to include api-proxy IP', () => {
216+
const configWithProxy = { ...mockConfig, enableApiProxy: true, anthropicApiKey: 'sk-ant-test-key' };
217+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
218+
const agent = result.services.agent;
219+
const env = agent.environment as Record<string, string>;
220+
expect(env.NO_PROXY).toContain('172.30.0.30');
221+
expect(env.no_proxy).toContain('172.30.0.30');
222+
});
223+
224+
it('should set CLAUDE_CODE_API_KEY_HELPER when Anthropic key is provided', () => {
225+
const configWithProxy = { ...mockConfig, enableApiProxy: true, anthropicApiKey: 'sk-ant-test-key' };
226+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
227+
const agent = result.services.agent;
228+
const env = agent.environment as Record<string, string>;
229+
expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh');
230+
});
231+
232+
it('should not set CLAUDE_CODE_API_KEY_HELPER when only OpenAI key is provided', () => {
233+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' };
234+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
235+
const agent = result.services.agent;
236+
const env = agent.environment as Record<string, string>;
237+
expect(env.CLAUDE_CODE_API_KEY_HELPER).toBeUndefined();
238+
});
239+
});

0 commit comments

Comments
 (0)