Skip to content

Commit e8c9d5f

Browse files
Mossakaclaude
andauthored
test: add DNS restriction enforcement tests (#1054)
* feat(api-proxy): add structured logging, metrics, and request tracing - logging.js: structured JSON logging with request IDs (crypto.randomUUID), sanitizeForLog utility, zero external dependencies - metrics.js: in-memory counters (requests_total, bytes), histograms (request_duration_ms with fixed buckets and percentile calculation), gauges (active_requests, uptime), memory-bounded - server.js: replace all console.log/error with structured logger, instrument proxyRequest() with full metrics, add X-Request-ID header propagation, enhance /health with metrics_summary, add GET /metrics endpoint on port 10000 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(api-proxy): add sliding window rate limiter with CLI integration Implement per-provider rate limiting for the API proxy sidecar: - rate-limiter.js: Sliding window counter algorithm with 1-second granularity for RPM/bytes and 1-minute granularity for RPH. Per-provider independence, memory-bounded, fail-open on errors. - server.js: Rate limit check before each proxyRequest() call. Returns 429 with Retry-After, X-RateLimit-* headers and JSON body. Rate limit status added to /health endpoint. - CLI flags: --rate-limit-rpm, --rate-limit-rph, --rate-limit-bytes-pm, --no-rate-limit (all require --enable-api-proxy) - TypeScript: RateLimitConfig interface in types.ts, env var passthrough in docker-manager.ts, validation in cli.ts - Test runner: AwfOptions extended with rate limit fields Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: add API proxy unit tests to build workflow Add Jest devDependency and test script to api-proxy package.json, and add a CI step in build.yml to run container-level unit tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add integration tests for api-proxy observability Add two integration test files that verify the observability and rate limiting features work end-to-end with actual Docker containers. api-proxy-observability.test.ts: - /metrics endpoint returns valid JSON with counters, histograms, gauges - /health endpoint includes metrics_summary - X-Request-ID header in proxy responses - Metrics increment after API requests - rate_limits appear in /health api-proxy-rate-limit.test.ts: - 429 response when RPM limit exceeded - Retry-After header in 429 response - X-RateLimit-* headers in 429 response - --no-rate-limit flag disables limiting - Custom RPM reflected in /health - Rate limit metrics in /metrics after rejection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: extract buildRateLimitConfig and add coverage tests Refactor rate limit validation into a standalone exported function that can be tested independently. Adds 12 unit tests covering defaults, --no-rate-limit, custom values, and validation errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add --block-domains integration tests Add blockDomains option to AwfRunner test fixture and integration tests for the --block-domains deny-list feature: - Block specific subdomain while allowing parent domain - Block takes precedence over allow - Wildcard blocking patterns (*.github.com) - Multiple blocked domains - Debug output verification Closes #1041 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add DNS restriction enforcement tests Add integration tests that verify DNS queries to non-whitelisted servers are actually blocked by the --dns-servers flag, closing a gap where no test used the dnsServers option in AwfRunner. New tests verify: - DNS queries to non-whitelisted servers are blocked - DNS queries to whitelisted servers succeed - The --dns-servers flag is passed through to iptables configuration - Default DNS (8.8.8.8, 8.8.4.4) works without explicit --dns-servers - Non-default DNS servers are blocked when using defaults - Cloudflare DNS works when explicitly whitelisted Closes #1043 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review feedback from Copilot - Fix api-proxy Dockerfile to copy logging.js, metrics.js, rate-limiter.js - Remove incomplete X-RateLimit headers test (covered by 429 test) - Remove loose DNS test assertion that always matched "dns-test" - Add CLI warning when rate limit flags used without --enable-api-proxy - Fix rate-limiter.js comment to match actual algorithm (rolling window) - Fix pre-existing cli.test.ts Commander.js parse failure Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add hasRateLimitOptions coverage to fix coverage regression Extract rate limit option detection into testable hasRateLimitOptions() function and add unit tests covering all branches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add validateApiProxyConfig copilot key coverage Add tests for hasCopilotKey branch that was previously untested, improving cli.ts line coverage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: restore rate limit options in awf-runner test helper The merge with main incorrectly dropped the rate limit options (rateLimitRpm, rateLimitRph, rateLimitBytesPm, noRateLimit) from AwfOptions and both run/runWithSudo methods. These are needed by api-proxy-rate-limit.test.ts on this branch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f113414 commit e8c9d5f

File tree

3 files changed

+140
-5
lines changed

3 files changed

+140
-5
lines changed

src/cli.test.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ describe('cli', () => {
351351
.option('--env-all', 'Pass all env vars', false);
352352

353353
// Parse empty args to get defaults
354-
program.parse(['node', 'awf'], { from: 'user' });
354+
program.parse([], { from: 'user' });
355355
const opts = program.opts();
356356

357357
expect(opts.logLevel).toBe('info');
@@ -1396,13 +1396,22 @@ describe('cli', () => {
13961396
expect(result.debugMessages[0]).toContain('Anthropic');
13971397
});
13981398

1399-
it('should detect both keys', () => {
1400-
const result = validateApiProxyConfig(true, true, true);
1399+
it('should detect Copilot key', () => {
1400+
const result = validateApiProxyConfig(true, false, false, true);
14011401
expect(result.enabled).toBe(true);
14021402
expect(result.warnings).toEqual([]);
1403-
expect(result.debugMessages).toHaveLength(2);
1403+
expect(result.debugMessages).toHaveLength(1);
1404+
expect(result.debugMessages[0]).toContain('Copilot');
1405+
});
1406+
1407+
it('should detect all three keys', () => {
1408+
const result = validateApiProxyConfig(true, true, true, true);
1409+
expect(result.enabled).toBe(true);
1410+
expect(result.warnings).toEqual([]);
1411+
expect(result.debugMessages).toHaveLength(3);
14041412
expect(result.debugMessages[0]).toContain('OpenAI');
14051413
expect(result.debugMessages[1]).toContain('Anthropic');
1414+
expect(result.debugMessages[2]).toContain('Copilot');
14061415
});
14071416

14081417
it('should not warn when disabled even with keys', () => {

src/cli.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,19 @@ export interface FlagValidationResult {
371371
error?: string;
372372
}
373373

374+
/**
375+
* Checks if any rate limit options are set in the CLI options.
376+
* Used to warn when rate limit flags are provided without --enable-api-proxy.
377+
*/
378+
export function hasRateLimitOptions(options: {
379+
rateLimitRpm?: string;
380+
rateLimitRph?: string;
381+
rateLimitBytesPm?: string;
382+
rateLimit?: boolean;
383+
}): boolean {
384+
return !!(options.rateLimitRpm || options.rateLimitRph || options.rateLimitBytesPm || options.rateLimit === false);
385+
}
386+
374387
/**
375388
* Validates that --skip-pull is not used with --build-local
376389
* @param skipPull - Whether --skip-pull flag was provided

tests/integration/dns-servers.test.ts

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
/// <reference path="../jest-custom-matchers.d.ts" />
1212

13-
import { describe, test, expect, beforeAll, afterAll } from '@jest/globals';
13+
import { describe, test, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
1414
import { createRunner, AwfRunner } from '../fixtures/awf-runner';
1515
import { cleanup } from '../fixtures/cleanup';
1616

@@ -113,3 +113,116 @@ describe('DNS Server Configuration', () => {
113113
expect(result.stdout.trim()).toMatch(/\d+\.\d+\.\d+\.\d+/);
114114
}, 120000);
115115
});
116+
117+
describe('DNS Restriction Enforcement', () => {
118+
let runner: AwfRunner;
119+
120+
beforeAll(async () => {
121+
await cleanup(false);
122+
runner = createRunner();
123+
});
124+
125+
afterAll(async () => {
126+
await cleanup(false);
127+
});
128+
129+
// Clean up between each test to prevent container name conflicts
130+
beforeEach(async () => {
131+
await cleanup(false);
132+
});
133+
134+
test('should block DNS queries to non-whitelisted servers', async () => {
135+
// Only whitelist Google DNS (8.8.8.8) — Cloudflare (1.1.1.1) should be blocked
136+
const result = await runner.runWithSudo(
137+
'nslookup example.com 1.1.1.1',
138+
{
139+
allowDomains: ['example.com'],
140+
dnsServers: ['8.8.8.8'],
141+
logLevel: 'debug',
142+
timeout: 60000,
143+
}
144+
);
145+
146+
// DNS query to non-whitelisted server should fail
147+
expect(result).toFail();
148+
}, 120000);
149+
150+
test('should allow DNS queries to whitelisted servers', async () => {
151+
// Whitelist Google DNS (8.8.8.8) — queries to it should succeed
152+
const result = await runner.runWithSudo(
153+
'nslookup example.com 8.8.8.8',
154+
{
155+
allowDomains: ['example.com'],
156+
dnsServers: ['8.8.8.8'],
157+
logLevel: 'debug',
158+
timeout: 60000,
159+
}
160+
);
161+
162+
expect(result).toSucceed();
163+
expect(result.stdout).toContain('Address');
164+
}, 120000);
165+
166+
test('should pass --dns-servers flag through to iptables configuration', async () => {
167+
const result = await runner.runWithSudo(
168+
'echo "dns-test"',
169+
{
170+
allowDomains: ['example.com'],
171+
dnsServers: ['8.8.8.8'],
172+
logLevel: 'debug',
173+
timeout: 60000,
174+
}
175+
);
176+
177+
expect(result).toSucceed();
178+
// Debug output should show the custom DNS server configuration
179+
expect(result.stderr).toContain('8.8.8.8');
180+
}, 120000);
181+
182+
test('should work with default DNS when --dns-servers is not specified', async () => {
183+
// Without explicit dnsServers, default Google DNS (8.8.8.8, 8.8.4.4) should work
184+
const result = await runner.runWithSudo(
185+
'nslookup example.com',
186+
{
187+
allowDomains: ['example.com'],
188+
logLevel: 'debug',
189+
timeout: 60000,
190+
}
191+
);
192+
193+
expect(result).toSucceed();
194+
expect(result.stdout).toContain('Address');
195+
}, 120000);
196+
197+
test('should block DNS to non-default server when using defaults', async () => {
198+
// With default DNS (8.8.8.8, 8.8.4.4), a query to a random DNS server
199+
// like 208.67.222.222 (OpenDNS) should be blocked
200+
const result = await runner.runWithSudo(
201+
'nslookup example.com 208.67.222.222',
202+
{
203+
allowDomains: ['example.com'],
204+
logLevel: 'debug',
205+
timeout: 60000,
206+
}
207+
);
208+
209+
// DNS query to non-default server should fail
210+
expect(result).toFail();
211+
}, 120000);
212+
213+
test('should allow Cloudflare DNS when explicitly whitelisted', async () => {
214+
// Whitelist Cloudflare DNS (1.1.1.1) — queries to it should succeed
215+
const result = await runner.runWithSudo(
216+
'nslookup example.com 1.1.1.1',
217+
{
218+
allowDomains: ['example.com'],
219+
dnsServers: ['1.1.1.1'],
220+
logLevel: 'debug',
221+
timeout: 60000,
222+
}
223+
);
224+
225+
expect(result).toSucceed();
226+
expect(result.stdout).toContain('Address');
227+
}, 120000);
228+
});

0 commit comments

Comments
 (0)