-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathserver.test.js
More file actions
268 lines (219 loc) · 10.3 KB
/
Copy pathserver.test.js
File metadata and controls
268 lines (219 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
/**
* Tests for api-proxy server.js
*/
const { deriveCopilotApiTarget, normalizeBasePath, buildUpstreamPath } = require('./server');
describe('deriveCopilotApiTarget', () => {
let originalEnv;
beforeEach(() => {
// Save original env vars
originalEnv = {
COPILOT_API_TARGET: process.env.COPILOT_API_TARGET,
GITHUB_SERVER_URL: process.env.GITHUB_SERVER_URL,
};
// Clear env vars before each test
delete process.env.COPILOT_API_TARGET;
delete process.env.GITHUB_SERVER_URL;
});
afterEach(() => {
// Restore original env vars
if (originalEnv.COPILOT_API_TARGET !== undefined) {
process.env.COPILOT_API_TARGET = originalEnv.COPILOT_API_TARGET;
} else {
delete process.env.COPILOT_API_TARGET;
}
if (originalEnv.GITHUB_SERVER_URL !== undefined) {
process.env.GITHUB_SERVER_URL = originalEnv.GITHUB_SERVER_URL;
} else {
delete process.env.GITHUB_SERVER_URL;
}
});
describe('COPILOT_API_TARGET env var (highest priority)', () => {
it('should return COPILOT_API_TARGET when explicitly set', () => {
process.env.COPILOT_API_TARGET = 'custom.api.com';
expect(deriveCopilotApiTarget()).toBe('custom.api.com');
});
it('should prefer COPILOT_API_TARGET over GITHUB_SERVER_URL', () => {
process.env.COPILOT_API_TARGET = 'custom.api.com';
process.env.GITHUB_SERVER_URL = 'https://mycompany.ghe.com';
expect(deriveCopilotApiTarget()).toBe('custom.api.com');
});
});
describe('GitHub Enterprise Cloud (*.ghe.com)', () => {
it('should derive copilot-api.<subdomain>.ghe.com for GHEC tenants', () => {
process.env.GITHUB_SERVER_URL = 'https://mycompany.ghe.com';
expect(deriveCopilotApiTarget()).toBe('copilot-api.mycompany.ghe.com');
});
it('should handle GHEC URLs with trailing slash', () => {
process.env.GITHUB_SERVER_URL = 'https://example.ghe.com/';
expect(deriveCopilotApiTarget()).toBe('copilot-api.example.ghe.com');
});
it('should handle GHEC URLs with path components', () => {
process.env.GITHUB_SERVER_URL = 'https://acme.ghe.com/some/path';
expect(deriveCopilotApiTarget()).toBe('copilot-api.acme.ghe.com');
});
it('should handle multi-part subdomain for GHEC', () => {
process.env.GITHUB_SERVER_URL = 'https://dev.mycompany.ghe.com';
expect(deriveCopilotApiTarget()).toBe('copilot-api.dev.mycompany.ghe.com');
});
});
describe('GitHub Enterprise Server (GHES)', () => {
it('should return api.enterprise.githubcopilot.com for GHES', () => {
process.env.GITHUB_SERVER_URL = 'https://github.example.com';
expect(deriveCopilotApiTarget()).toBe('api.enterprise.githubcopilot.com');
});
it('should handle GHES with IP address', () => {
process.env.GITHUB_SERVER_URL = 'https://192.168.1.100';
expect(deriveCopilotApiTarget()).toBe('api.enterprise.githubcopilot.com');
});
it('should handle GHES with custom port', () => {
process.env.GITHUB_SERVER_URL = 'https://github.internal:8443';
expect(deriveCopilotApiTarget()).toBe('api.enterprise.githubcopilot.com');
});
});
describe('GitHub.com (public)', () => {
it('should return api.githubcopilot.com for github.com', () => {
process.env.GITHUB_SERVER_URL = 'https://github.com';
expect(deriveCopilotApiTarget()).toBe('api.githubcopilot.com');
});
it('should handle github.com with trailing slash', () => {
process.env.GITHUB_SERVER_URL = 'https://github.com/';
expect(deriveCopilotApiTarget()).toBe('api.githubcopilot.com');
});
it('should handle github.com with path', () => {
process.env.GITHUB_SERVER_URL = 'https://github.com/github/hub';
expect(deriveCopilotApiTarget()).toBe('api.githubcopilot.com');
});
});
describe('Default behavior', () => {
it('should return api.githubcopilot.com when no env vars are set', () => {
expect(deriveCopilotApiTarget()).toBe('api.githubcopilot.com');
});
it('should return default when GITHUB_SERVER_URL is empty string', () => {
process.env.GITHUB_SERVER_URL = '';
expect(deriveCopilotApiTarget()).toBe('api.githubcopilot.com');
});
it('should return default when GITHUB_SERVER_URL is invalid', () => {
process.env.GITHUB_SERVER_URL = 'not-a-valid-url';
expect(deriveCopilotApiTarget()).toBe('api.githubcopilot.com');
});
it('should return default when GITHUB_SERVER_URL is malformed', () => {
process.env.GITHUB_SERVER_URL = 'ht!tp://bad-url';
expect(deriveCopilotApiTarget()).toBe('api.githubcopilot.com');
});
});
});
describe('normalizeBasePath', () => {
it('should return empty string for undefined', () => {
expect(normalizeBasePath(undefined)).toBe('');
});
it('should return empty string for null', () => {
expect(normalizeBasePath(null)).toBe('');
});
it('should return empty string for empty string', () => {
expect(normalizeBasePath('')).toBe('');
});
it('should return empty string for whitespace-only string', () => {
expect(normalizeBasePath(' ')).toBe('');
});
it('should preserve a well-formed path', () => {
expect(normalizeBasePath('/serving-endpoints')).toBe('/serving-endpoints');
});
it('should add leading slash when missing', () => {
expect(normalizeBasePath('serving-endpoints')).toBe('/serving-endpoints');
});
it('should strip trailing slash', () => {
expect(normalizeBasePath('/serving-endpoints/')).toBe('/serving-endpoints');
});
it('should handle multi-segment paths', () => {
expect(normalizeBasePath('/openai/deployments/gpt-4')).toBe('/openai/deployments/gpt-4');
});
it('should normalize a path missing the leading slash and with trailing slash', () => {
expect(normalizeBasePath('openai/deployments/gpt-4/')).toBe('/openai/deployments/gpt-4');
});
it('should preserve a root-only path', () => {
expect(normalizeBasePath('/')).toBe('/');
});
});
describe('buildUpstreamPath', () => {
const HOST = 'api.example.com';
describe('no base path (empty string)', () => {
it('should return the request path unchanged when basePath is empty', () => {
expect(buildUpstreamPath('/v1/chat/completions', HOST, '')).toBe('/v1/chat/completions');
});
it('should preserve query string when basePath is empty', () => {
expect(buildUpstreamPath('/v1/chat/completions?stream=true', HOST, '')).toBe('/v1/chat/completions?stream=true');
});
it('should preserve multiple query params when basePath is empty', () => {
expect(buildUpstreamPath('/v1/models?limit=10&order=asc', HOST, '')).toBe('/v1/models?limit=10&order=asc');
});
it('should handle root path with no base path', () => {
expect(buildUpstreamPath('/', HOST, '')).toBe('/');
});
});
describe('Databricks serving-endpoints (single-segment base path)', () => {
it('should prepend /serving-endpoints to chat completions path', () => {
expect(buildUpstreamPath('/v1/chat/completions', HOST, '/serving-endpoints'))
.toBe('/serving-endpoints/v1/chat/completions');
});
it('should prepend /serving-endpoints and preserve query string', () => {
expect(buildUpstreamPath('/v1/chat/completions?stream=true', HOST, '/serving-endpoints'))
.toBe('/serving-endpoints/v1/chat/completions?stream=true');
});
it('should prepend /serving-endpoints to models path', () => {
expect(buildUpstreamPath('/v1/models', HOST, '/serving-endpoints'))
.toBe('/serving-endpoints/v1/models');
});
it('should prepend /serving-endpoints to embeddings path', () => {
expect(buildUpstreamPath('/v1/embeddings', HOST, '/serving-endpoints'))
.toBe('/serving-endpoints/v1/embeddings');
});
});
describe('Azure OpenAI deployments (multi-segment base path)', () => {
it('should prepend Azure deployment path to chat completions', () => {
expect(buildUpstreamPath('/chat/completions', HOST, '/openai/deployments/gpt-4'))
.toBe('/openai/deployments/gpt-4/chat/completions');
});
it('should prepend Azure deployment path and preserve api-version query param', () => {
expect(buildUpstreamPath('/chat/completions?api-version=2024-02-01', HOST, '/openai/deployments/gpt-4'))
.toBe('/openai/deployments/gpt-4/chat/completions?api-version=2024-02-01');
});
it('should handle a deeply nested Azure deployment name', () => {
expect(buildUpstreamPath('/chat/completions', HOST, '/openai/deployments/my-custom-gpt-4-deployment'))
.toBe('/openai/deployments/my-custom-gpt-4-deployment/chat/completions');
});
});
describe('Anthropic custom target with base path', () => {
it('should prepend /anthropic to messages endpoint', () => {
expect(buildUpstreamPath('/v1/messages', 'proxy.corporate.com', '/anthropic'))
.toBe('/anthropic/v1/messages');
});
it('should preserve Anthropic query params', () => {
expect(buildUpstreamPath('/v1/messages?beta=true', 'proxy.corporate.com', '/anthropic'))
.toBe('/anthropic/v1/messages?beta=true');
});
});
describe('path preservation for real-world API endpoints', () => {
it('should preserve /v1/chat/completions exactly (OpenAI standard path)', () => {
expect(buildUpstreamPath('/v1/chat/completions', 'api.openai.com', ''))
.toBe('/v1/chat/completions');
});
it('should preserve /v1/messages exactly (Anthropic standard path)', () => {
expect(buildUpstreamPath('/v1/messages', 'api.anthropic.com', ''))
.toBe('/v1/messages');
});
it('should handle URL-encoded characters in path', () => {
// %2F is preserved by the URL parser (an encoded slash stays encoded)
expect(buildUpstreamPath('/v1/models/gpt-4%2Fturbo', HOST, '/serving-endpoints'))
.toBe('/serving-endpoints/v1/models/gpt-4%2Fturbo');
});
it('should handle hash fragment being ignored (not forwarded in HTTP requests)', () => {
// Hash fragments are never sent to the server; URL parser drops them
expect(buildUpstreamPath('/v1/chat/completions#fragment', HOST, '/serving-endpoints'))
.toBe('/serving-endpoints/v1/chat/completions');
});
it('should preserve empty query string marker', () => {
expect(buildUpstreamPath('/v1/chat/completions?', HOST, '/serving-endpoints'))
.toBe('/serving-endpoints/v1/chat/completions');
});
});
});