Skip to content

Commit 43f446f

Browse files
Claudelpcox
andcommitted
refactor(api-proxy): export function for tests and improve docs
- Export deriveCopilotApiTarget for testing (remove duplicate) - Update COPILOT_API_TARGET docs to show both flag and env sources - Use wildcard domain patterns in GHEC examples - Add domain matching explanation Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
1 parent 085cf29 commit 43f446f

File tree

3 files changed

+45
-117
lines changed

3 files changed

+45
-117
lines changed

containers/api-proxy/server.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,13 @@ function handleManagementEndpoint(req, res) {
395395
return false;
396396
}
397397

398+
// Export for testing
399+
module.exports = {
400+
deriveCopilotApiTarget,
401+
};
402+
403+
// Only start servers if this file is run directly (not required for tests)
404+
if (require.main === module) {
398405
// Health port is always 10000 — this is what Docker healthcheck hits
399406
const HEALTH_PORT = 10000;
400407

@@ -514,3 +521,5 @@ process.on('SIGINT', () => {
514521
logRequest('info', 'shutdown', { message: 'Received SIGINT, shutting down gracefully' });
515522
process.exit(0);
516523
});
524+
525+
} // End of if (require.main === module)

containers/api-proxy/server.test.js

Lines changed: 29 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* Tests for API Proxy Server functions
33
*/
44

5+
const { deriveCopilotApiTarget } = require('./server');
6+
57
describe('deriveCopilotApiTarget', () => {
68
let originalEnv;
79

@@ -15,166 +17,79 @@ describe('deriveCopilotApiTarget', () => {
1517
process.env = originalEnv;
1618
});
1719

18-
// Helper function to reload the module and get the derived target
19-
function getDerivedTarget(env = {}) {
20-
// Set environment variables
21-
Object.keys(env).forEach(key => {
22-
process.env[key] = env[key];
23-
});
24-
25-
// Clear module cache to force re-evaluation
26-
delete require.cache[require.resolve('./server.js')];
27-
28-
// Mock the required modules that have side effects
29-
jest.mock('http', () => ({
30-
createServer: jest.fn(() => ({
31-
listen: jest.fn(),
32-
})),
33-
}));
34-
35-
jest.mock('https', () => ({
36-
request: jest.fn(),
37-
}));
38-
39-
jest.mock('./logging', () => ({
40-
generateRequestId: jest.fn(() => 'test-id'),
41-
sanitizeForLog: jest.fn(x => x),
42-
logRequest: jest.fn(),
43-
}));
44-
45-
jest.mock('./metrics', () => ({
46-
increment: jest.fn(),
47-
gaugeInc: jest.fn(),
48-
gaugeDec: jest.fn(),
49-
observe: jest.fn(),
50-
statusClass: jest.fn(() => '2xx'),
51-
getSummary: jest.fn(() => ({})),
52-
getMetrics: jest.fn(() => ({})),
53-
}));
54-
55-
jest.mock('./rate-limiter', () => ({
56-
create: jest.fn(() => ({
57-
check: jest.fn(() => ({ allowed: true })),
58-
getAllStatus: jest.fn(() => ({})),
59-
})),
60-
}));
61-
62-
// We can't easily extract the function since it's not exported,
63-
// but we can test via the startup logs which log COPILOT_API_TARGET
64-
// For now, let's create a standalone version to test
65-
return deriveCopilotApiTargetStandalone(env);
66-
}
67-
68-
// Standalone version of the function for testing
69-
function deriveCopilotApiTargetStandalone(env) {
70-
const COPILOT_API_TARGET = env.COPILOT_API_TARGET;
71-
const GITHUB_SERVER_URL = env.GITHUB_SERVER_URL;
72-
73-
if (COPILOT_API_TARGET) {
74-
return COPILOT_API_TARGET;
75-
}
76-
77-
if (GITHUB_SERVER_URL) {
78-
try {
79-
const hostname = new URL(GITHUB_SERVER_URL).hostname;
80-
if (hostname !== 'github.com') {
81-
// For GitHub Enterprise Cloud with data residency (*.ghe.com),
82-
// derive the API endpoint as api.SUBDOMAIN.ghe.com
83-
if (hostname.endsWith('.ghe.com')) {
84-
const subdomain = hostname.replace('.ghe.com', '');
85-
return `api.${subdomain}.ghe.com`;
86-
}
87-
// For other enterprise hosts (GHES), use the generic enterprise endpoint
88-
return 'api.enterprise.githubcopilot.com';
89-
}
90-
} catch {
91-
// Invalid URL — fall through to default
92-
}
93-
}
94-
return 'api.githubcopilot.com';
95-
}
96-
9720
test('should return default api.githubcopilot.com when no env vars set', () => {
98-
const target = getDerivedTarget({});
21+
delete process.env.COPILOT_API_TARGET;
22+
delete process.env.GITHUB_SERVER_URL;
23+
24+
const target = deriveCopilotApiTarget();
9925
expect(target).toBe('api.githubcopilot.com');
10026
});
10127

10228
test('should use COPILOT_API_TARGET when explicitly set', () => {
103-
const target = getDerivedTarget({
104-
COPILOT_API_TARGET: 'custom.api.example.com',
105-
});
29+
process.env.COPILOT_API_TARGET = 'custom.api.example.com';
30+
const target = deriveCopilotApiTarget();
10631
expect(target).toBe('custom.api.example.com');
10732
});
10833

10934
test('should prioritize COPILOT_API_TARGET over GITHUB_SERVER_URL', () => {
110-
const target = getDerivedTarget({
111-
COPILOT_API_TARGET: 'custom.api.example.com',
112-
GITHUB_SERVER_URL: 'https://mycompany.ghe.com',
113-
});
35+
process.env.COPILOT_API_TARGET = 'custom.api.example.com';
36+
process.env.GITHUB_SERVER_URL = 'https://mycompany.ghe.com';
37+
const target = deriveCopilotApiTarget();
11438
expect(target).toBe('custom.api.example.com');
11539
});
11640

11741
test('should return api.githubcopilot.com for github.com', () => {
118-
const target = getDerivedTarget({
119-
GITHUB_SERVER_URL: 'https://github.com',
120-
});
42+
process.env.GITHUB_SERVER_URL = 'https://github.com';
43+
const target = deriveCopilotApiTarget();
12144
expect(target).toBe('api.githubcopilot.com');
12245
});
12346

12447
test('should derive api.SUBDOMAIN.ghe.com for *.ghe.com domains', () => {
125-
const target = getDerivedTarget({
126-
GITHUB_SERVER_URL: 'https://mycompany.ghe.com',
127-
});
48+
process.env.GITHUB_SERVER_URL = 'https://mycompany.ghe.com';
49+
const target = deriveCopilotApiTarget();
12850
expect(target).toBe('api.mycompany.ghe.com');
12951
});
13052

13153
test('should derive api.SUBDOMAIN.ghe.com for different *.ghe.com subdomain', () => {
132-
const target = getDerivedTarget({
133-
GITHUB_SERVER_URL: 'https://acme-corp.ghe.com',
134-
});
54+
process.env.GITHUB_SERVER_URL = 'https://acme-corp.ghe.com';
55+
const target = deriveCopilotApiTarget();
13556
expect(target).toBe('api.acme-corp.ghe.com');
13657
});
13758

13859
test('should use api.enterprise.githubcopilot.com for GHES (non-.ghe.com enterprise)', () => {
139-
const target = getDerivedTarget({
140-
GITHUB_SERVER_URL: 'https://github.enterprise.com',
141-
});
60+
process.env.GITHUB_SERVER_URL = 'https://github.enterprise.com';
61+
const target = deriveCopilotApiTarget();
14262
expect(target).toBe('api.enterprise.githubcopilot.com');
14363
});
14464

14565
test('should use api.enterprise.githubcopilot.com for custom GHES domain', () => {
146-
const target = getDerivedTarget({
147-
GITHUB_SERVER_URL: 'https://git.mycompany.com',
148-
});
66+
process.env.GITHUB_SERVER_URL = 'https://git.mycompany.com';
67+
const target = deriveCopilotApiTarget();
14968
expect(target).toBe('api.enterprise.githubcopilot.com');
15069
});
15170

15271
test('should handle GITHUB_SERVER_URL without protocol gracefully', () => {
153-
const target = getDerivedTarget({
154-
GITHUB_SERVER_URL: 'mycompany.ghe.com',
155-
});
72+
process.env.GITHUB_SERVER_URL = 'mycompany.ghe.com';
73+
const target = deriveCopilotApiTarget();
15674
// Invalid URL, should fall back to default
15775
expect(target).toBe('api.githubcopilot.com');
15876
});
15977

16078
test('should handle invalid GITHUB_SERVER_URL gracefully', () => {
161-
const target = getDerivedTarget({
162-
GITHUB_SERVER_URL: 'not-a-valid-url',
163-
});
79+
process.env.GITHUB_SERVER_URL = 'not-a-valid-url';
80+
const target = deriveCopilotApiTarget();
16481
expect(target).toBe('api.githubcopilot.com');
16582
});
16683

16784
test('should handle GITHUB_SERVER_URL with port', () => {
168-
const target = getDerivedTarget({
169-
GITHUB_SERVER_URL: 'https://mycompany.ghe.com:443',
170-
});
85+
process.env.GITHUB_SERVER_URL = 'https://mycompany.ghe.com:443';
86+
const target = deriveCopilotApiTarget();
17187
expect(target).toBe('api.mycompany.ghe.com');
17288
});
17389

17490
test('should handle GITHUB_SERVER_URL with path', () => {
175-
const target = getDerivedTarget({
176-
GITHUB_SERVER_URL: 'https://mycompany.ghe.com/some/path',
177-
});
91+
process.env.GITHUB_SERVER_URL = 'https://mycompany.ghe.com/some/path';
92+
const target = deriveCopilotApiTarget();
17893
expect(target).toBe('api.mycompany.ghe.com');
17994
});
18095
});

docs/api-proxy-sidecar.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export COPILOT_GITHUB_TOKEN="your-token"
110110
export GITHUB_SERVER_URL="https://mycompany.ghe.com"
111111

112112
sudo -E awf --enable-api-proxy \
113-
--allow-domains api.mycompany.ghe.com,mycompany.ghe.com \
113+
--allow-domains '*.mycompany.ghe.com' \
114114
-- npx @github/copilot --prompt "your prompt"
115115
```
116116

@@ -120,6 +120,10 @@ sudo -E awf --enable-api-proxy \
120120
- Example: `mycompany.ghe.com``api.mycompany.ghe.com`
121121
- For other enterprise hosts (GHES), it routes to `api.enterprise.githubcopilot.com`
122122

123+
**Domain matching:**
124+
- `*.mycompany.ghe.com` matches all subdomains (e.g., `api.mycompany.ghe.com`, `github.mycompany.ghe.com`)
125+
- Add additional domains only if your workflow needs to access other services (e.g., `mycompany.ghe.com` for the base domain)
126+
123127
**Important:** Use `sudo -E` to preserve the `GITHUB_SERVER_URL` environment variable when running awf.
124128

125129
You can also explicitly set the Copilot API target:
@@ -128,7 +132,7 @@ You can also explicitly set the Copilot API target:
128132
export COPILOT_API_TARGET="api.mycompany.ghe.com"
129133
sudo -E awf --enable-api-proxy \
130134
--copilot-api-target api.mycompany.ghe.com \
131-
--allow-domains api.mycompany.ghe.com,mycompany.ghe.com \
135+
--allow-domains '*.mycompany.ghe.com' \
132136
-- npx @github/copilot --prompt "your prompt"
133137
```
134138

@@ -154,7 +158,7 @@ The API proxy sidecar receives **real credentials** and routing configuration:
154158
| `OPENAI_API_KEY` | Real API key | `--enable-api-proxy` and env set | OpenAI API key (injected into requests) |
155159
| `ANTHROPIC_API_KEY` | Real API key | `--enable-api-proxy` and env set | Anthropic API key (injected into requests) |
156160
| `COPILOT_GITHUB_TOKEN` | Real token | `--enable-api-proxy` and env set | GitHub Copilot token (injected into requests) |
157-
| `COPILOT_API_TARGET` | Target hostname | `--copilot-api-target` flag | Override Copilot API endpoint (default: auto-derived) |
161+
| `COPILOT_API_TARGET` | Target hostname | `--copilot-api-target` flag or host env `COPILOT_API_TARGET` | Override Copilot API endpoint (default: auto-derived) |
158162
| `GITHUB_SERVER_URL` | GitHub server URL | Passed from host env | Auto-derives Copilot API endpoint for enterprise (*.ghe.com → api.SUBDOMAIN.ghe.com) |
159163
| `HTTP_PROXY` | `http://172.30.0.10:3128` | Always | Routes through Squid for domain filtering |
160164
| `HTTPS_PROXY` | `http://172.30.0.10:3128` | Always | Routes through Squid for domain filtering |

0 commit comments

Comments
 (0)