@cloudflare/sandbox 0.0.0-02ee8fe
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +311 -0
- package/Dockerfile +143 -0
- package/README.md +162 -0
- package/dist/chunk-BFVUNTP4.js +104 -0
- package/dist/chunk-BFVUNTP4.js.map +1 -0
- package/dist/chunk-EKSWCBCA.js +86 -0
- package/dist/chunk-EKSWCBCA.js.map +1 -0
- package/dist/chunk-JXZMAU2C.js +559 -0
- package/dist/chunk-JXZMAU2C.js.map +1 -0
- package/dist/chunk-UJ3TV4M6.js +7 -0
- package/dist/chunk-UJ3TV4M6.js.map +1 -0
- package/dist/chunk-YE265ASX.js +2484 -0
- package/dist/chunk-YE265ASX.js.map +1 -0
- package/dist/chunk-Z532A7QC.js +78 -0
- package/dist/chunk-Z532A7QC.js.map +1 -0
- package/dist/file-stream.d.ts +43 -0
- package/dist/file-stream.js +9 -0
- package/dist/file-stream.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/interpreter.d.ts +33 -0
- package/dist/interpreter.js +8 -0
- package/dist/interpreter.js.map +1 -0
- package/dist/request-handler.d.ts +18 -0
- package/dist/request-handler.js +13 -0
- package/dist/request-handler.js.map +1 -0
- package/dist/sandbox-CLZWpfGc.d.ts +613 -0
- package/dist/sandbox.d.ts +4 -0
- package/dist/sandbox.js +13 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/security.d.ts +31 -0
- package/dist/security.js +13 -0
- package/dist/security.js.map +1 -0
- package/dist/sse-parser.d.ts +28 -0
- package/dist/sse-parser.js +11 -0
- package/dist/sse-parser.js.map +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/package.json +44 -0
- package/src/clients/base-client.ts +280 -0
- package/src/clients/command-client.ts +115 -0
- package/src/clients/file-client.ts +295 -0
- package/src/clients/git-client.ts +92 -0
- package/src/clients/index.ts +64 -0
- package/src/clients/interpreter-client.ts +329 -0
- package/src/clients/port-client.ts +105 -0
- package/src/clients/process-client.ts +177 -0
- package/src/clients/sandbox-client.ts +41 -0
- package/src/clients/types.ts +84 -0
- package/src/clients/utility-client.ts +119 -0
- package/src/errors/adapter.ts +180 -0
- package/src/errors/classes.ts +469 -0
- package/src/errors/index.ts +105 -0
- package/src/file-stream.ts +164 -0
- package/src/index.ts +93 -0
- package/src/interpreter.ts +159 -0
- package/src/request-handler.ts +180 -0
- package/src/sandbox.ts +1045 -0
- package/src/security.ts +104 -0
- package/src/sse-parser.ts +143 -0
- package/src/version.ts +6 -0
- package/startup.sh +3 -0
- package/tests/base-client.test.ts +328 -0
- package/tests/command-client.test.ts +407 -0
- package/tests/file-client.test.ts +719 -0
- package/tests/file-stream.test.ts +306 -0
- package/tests/get-sandbox.test.ts +149 -0
- package/tests/git-client.test.ts +328 -0
- package/tests/port-client.test.ts +301 -0
- package/tests/process-client.test.ts +658 -0
- package/tests/request-handler.test.ts +240 -0
- package/tests/sandbox.test.ts +554 -0
- package/tests/sse-parser.test.ts +290 -0
- package/tests/utility-client.test.ts +332 -0
- package/tests/version.test.ts +16 -0
- package/tests/wrangler.jsonc +35 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +31 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { GitCheckoutResponse } from '../src/clients';
|
|
3
|
+
import { GitClient } from '../src/clients/git-client';
|
|
4
|
+
import {
|
|
5
|
+
GitAuthenticationError,
|
|
6
|
+
GitBranchNotFoundError,
|
|
7
|
+
GitCheckoutError,
|
|
8
|
+
GitCloneError,
|
|
9
|
+
GitError,
|
|
10
|
+
GitNetworkError,
|
|
11
|
+
GitRepositoryNotFoundError,
|
|
12
|
+
InvalidGitUrlError,
|
|
13
|
+
SandboxError
|
|
14
|
+
} from '../src/errors';
|
|
15
|
+
|
|
16
|
+
describe('GitClient', () => {
|
|
17
|
+
let client: GitClient;
|
|
18
|
+
let mockFetch: ReturnType<typeof vi.fn>;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
|
|
23
|
+
mockFetch = vi.fn();
|
|
24
|
+
global.fetch = mockFetch as unknown as typeof fetch;
|
|
25
|
+
|
|
26
|
+
client = new GitClient({
|
|
27
|
+
baseUrl: 'http://test.com',
|
|
28
|
+
port: 3000,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.restoreAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('repository cloning', () => {
|
|
37
|
+
it('should clone public repositories successfully', async () => {
|
|
38
|
+
const mockResponse: GitCheckoutResponse = {
|
|
39
|
+
success: true,
|
|
40
|
+
stdout: 'Cloning into \'react\'...\nReceiving objects: 100% (1284/1284), done.',
|
|
41
|
+
stderr: '',
|
|
42
|
+
exitCode: 0,
|
|
43
|
+
repoUrl: 'https://github.com/facebook/react.git',
|
|
44
|
+
branch: 'main',
|
|
45
|
+
targetDir: 'react',
|
|
46
|
+
timestamp: '2023-01-01T00:00:00Z',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
|
|
50
|
+
|
|
51
|
+
const result = await client.checkout('https://github.com/facebook/react.git', 'test-session');
|
|
52
|
+
|
|
53
|
+
expect(result.success).toBe(true);
|
|
54
|
+
expect(result.repoUrl).toBe('https://github.com/facebook/react.git');
|
|
55
|
+
expect(result.branch).toBe('main');
|
|
56
|
+
expect(result.exitCode).toBe(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should clone repositories to specific branches', async () => {
|
|
60
|
+
const mockResponse: GitCheckoutResponse = {
|
|
61
|
+
success: true,
|
|
62
|
+
stdout: 'Cloning into \'project\'...\nSwitching to branch \'development\'',
|
|
63
|
+
stderr: '',
|
|
64
|
+
exitCode: 0,
|
|
65
|
+
repoUrl: 'https://github.com/company/project.git',
|
|
66
|
+
branch: 'development',
|
|
67
|
+
targetDir: 'project',
|
|
68
|
+
timestamp: '2023-01-01T00:00:00Z',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
|
|
72
|
+
|
|
73
|
+
const result = await client.checkout(
|
|
74
|
+
'https://github.com/company/project.git',
|
|
75
|
+
'test-session',
|
|
76
|
+
{ branch: 'development' }
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(result.success).toBe(true);
|
|
80
|
+
expect(result.branch).toBe('development');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should clone repositories to custom directories', async () => {
|
|
84
|
+
const mockResponse: GitCheckoutResponse = {
|
|
85
|
+
success: true,
|
|
86
|
+
stdout: 'Cloning into \'workspace/my-app\'...\nDone.',
|
|
87
|
+
stderr: '',
|
|
88
|
+
exitCode: 0,
|
|
89
|
+
repoUrl: 'https://github.com/user/my-app.git',
|
|
90
|
+
branch: 'main',
|
|
91
|
+
targetDir: 'workspace/my-app',
|
|
92
|
+
timestamp: '2023-01-01T00:00:00Z',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
|
|
96
|
+
|
|
97
|
+
const result = await client.checkout(
|
|
98
|
+
'https://github.com/user/my-app.git',
|
|
99
|
+
'test-session',
|
|
100
|
+
{ targetDir: 'workspace/my-app' }
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(result.success).toBe(true);
|
|
104
|
+
expect(result.targetDir).toBe('workspace/my-app');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should handle large repository clones with warnings', async () => {
|
|
108
|
+
const mockResponse: GitCheckoutResponse = {
|
|
109
|
+
success: true,
|
|
110
|
+
stdout: 'Cloning into \'linux\'...\nReceiving objects: 100% (8125432/8125432), 2.34 GiB, done.',
|
|
111
|
+
stderr: 'warning: filtering not recognized by server',
|
|
112
|
+
exitCode: 0,
|
|
113
|
+
repoUrl: 'https://github.com/torvalds/linux.git',
|
|
114
|
+
branch: 'master',
|
|
115
|
+
targetDir: 'linux',
|
|
116
|
+
timestamp: '2023-01-01T00:05:30Z',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
|
|
120
|
+
|
|
121
|
+
const result = await client.checkout('https://github.com/torvalds/linux.git', 'test-session');
|
|
122
|
+
|
|
123
|
+
expect(result.success).toBe(true);
|
|
124
|
+
expect(result.stderr).toContain('warning:');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle SSH repository URLs', async () => {
|
|
128
|
+
const mockResponse: GitCheckoutResponse = {
|
|
129
|
+
success: true,
|
|
130
|
+
stdout: 'Cloning into \'private-project\'...\nDone.',
|
|
131
|
+
stderr: '',
|
|
132
|
+
exitCode: 0,
|
|
133
|
+
repoUrl: 'git@github.com:company/private-project.git',
|
|
134
|
+
branch: 'main',
|
|
135
|
+
targetDir: 'private-project',
|
|
136
|
+
timestamp: '2023-01-01T00:00:00Z',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
|
|
140
|
+
|
|
141
|
+
const result = await client.checkout('git@github.com:company/private-project.git', 'test-session');
|
|
142
|
+
|
|
143
|
+
expect(result.success).toBe(true);
|
|
144
|
+
expect(result.repoUrl).toBe('git@github.com:company/private-project.git');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should handle concurrent repository operations', async () => {
|
|
148
|
+
mockFetch.mockImplementation((url: string, options: RequestInit) => {
|
|
149
|
+
const body = JSON.parse(options.body as string);
|
|
150
|
+
const repoName = body.repoUrl.split('/').pop().replace('.git', '');
|
|
151
|
+
|
|
152
|
+
return Promise.resolve(new Response(JSON.stringify({
|
|
153
|
+
success: true,
|
|
154
|
+
stdout: `Cloning into '${repoName}'...\nDone.`,
|
|
155
|
+
stderr: '',
|
|
156
|
+
exitCode: 0,
|
|
157
|
+
repoUrl: body.repoUrl,
|
|
158
|
+
branch: body.branch || 'main',
|
|
159
|
+
targetDir: body.targetDir || repoName,
|
|
160
|
+
timestamp: new Date().toISOString(),
|
|
161
|
+
})));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const operations = await Promise.all([
|
|
165
|
+
client.checkout('https://github.com/facebook/react.git', 'session-1'),
|
|
166
|
+
client.checkout('https://github.com/microsoft/vscode.git', 'session-2'),
|
|
167
|
+
client.checkout('https://github.com/nodejs/node.git', 'session-3', { branch: 'v18.x' }),
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
expect(operations).toHaveLength(3);
|
|
171
|
+
operations.forEach(result => {
|
|
172
|
+
expect(result.success).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('repository error handling', () => {
|
|
179
|
+
it('should handle repository not found errors', async () => {
|
|
180
|
+
mockFetch.mockResolvedValue(new Response(
|
|
181
|
+
JSON.stringify({ error: 'Repository not found', code: 'GIT_REPOSITORY_NOT_FOUND' }),
|
|
182
|
+
{ status: 404 }
|
|
183
|
+
));
|
|
184
|
+
|
|
185
|
+
await expect(client.checkout('https://github.com/user/nonexistent.git', 'test-session'))
|
|
186
|
+
.rejects.toThrow(GitRepositoryNotFoundError);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should handle authentication failures', async () => {
|
|
190
|
+
mockFetch.mockResolvedValue(new Response(
|
|
191
|
+
JSON.stringify({ error: 'Authentication failed', code: 'GIT_AUTH_FAILED' }),
|
|
192
|
+
{ status: 401 }
|
|
193
|
+
));
|
|
194
|
+
|
|
195
|
+
await expect(client.checkout('https://github.com/company/private.git', 'test-session'))
|
|
196
|
+
.rejects.toThrow(GitAuthenticationError);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should handle branch not found errors', async () => {
|
|
200
|
+
mockFetch.mockResolvedValue(new Response(
|
|
201
|
+
JSON.stringify({ error: 'Branch not found', code: 'GIT_BRANCH_NOT_FOUND' }),
|
|
202
|
+
{ status: 404 }
|
|
203
|
+
));
|
|
204
|
+
|
|
205
|
+
await expect(client.checkout(
|
|
206
|
+
'https://github.com/user/repo.git',
|
|
207
|
+
'test-session',
|
|
208
|
+
{ branch: 'nonexistent-branch' }
|
|
209
|
+
)).rejects.toThrow(GitBranchNotFoundError);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should handle network errors', async () => {
|
|
213
|
+
mockFetch.mockResolvedValue(new Response(
|
|
214
|
+
JSON.stringify({ error: 'Network error', code: 'GIT_NETWORK_ERROR' }),
|
|
215
|
+
{ status: 503 }
|
|
216
|
+
));
|
|
217
|
+
|
|
218
|
+
await expect(client.checkout('https://github.com/user/repo.git', 'test-session'))
|
|
219
|
+
.rejects.toThrow(GitNetworkError);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should handle clone failures', async () => {
|
|
223
|
+
mockFetch.mockResolvedValue(new Response(
|
|
224
|
+
JSON.stringify({ error: 'Clone failed', code: 'GIT_CLONE_FAILED' }),
|
|
225
|
+
{ status: 507 }
|
|
226
|
+
));
|
|
227
|
+
|
|
228
|
+
await expect(client.checkout('https://github.com/large/repository.git', 'test-session'))
|
|
229
|
+
.rejects.toThrow(GitCloneError);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should handle checkout failures', async () => {
|
|
233
|
+
mockFetch.mockResolvedValue(new Response(
|
|
234
|
+
JSON.stringify({ error: 'Checkout failed', code: 'GIT_CHECKOUT_FAILED' }),
|
|
235
|
+
{ status: 409 }
|
|
236
|
+
));
|
|
237
|
+
|
|
238
|
+
await expect(client.checkout(
|
|
239
|
+
'https://github.com/user/repo.git',
|
|
240
|
+
'test-session',
|
|
241
|
+
{ branch: 'feature-branch' }
|
|
242
|
+
)).rejects.toThrow(GitCheckoutError);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should handle invalid Git URLs', async () => {
|
|
246
|
+
mockFetch.mockResolvedValue(new Response(
|
|
247
|
+
JSON.stringify({ error: 'Invalid Git URL', code: 'INVALID_GIT_URL' }),
|
|
248
|
+
{ status: 400 }
|
|
249
|
+
));
|
|
250
|
+
|
|
251
|
+
await expect(client.checkout('not-a-valid-url', 'test-session'))
|
|
252
|
+
.rejects.toThrow(InvalidGitUrlError);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should handle partial clone failures', async () => {
|
|
256
|
+
const mockResponse: GitCheckoutResponse = {
|
|
257
|
+
success: false,
|
|
258
|
+
stdout: 'Cloning into \'repo\'...\nReceiving objects: 45% (450/1000)',
|
|
259
|
+
stderr: 'error: RPC failed\nfatal: early EOF',
|
|
260
|
+
exitCode: 128,
|
|
261
|
+
repoUrl: 'https://github.com/problematic/repo.git',
|
|
262
|
+
branch: 'main',
|
|
263
|
+
targetDir: 'repo',
|
|
264
|
+
timestamp: '2023-01-01T00:01:30Z',
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
|
|
268
|
+
|
|
269
|
+
const result = await client.checkout('https://github.com/problematic/repo.git', 'test-session');
|
|
270
|
+
|
|
271
|
+
expect(result.success).toBe(false);
|
|
272
|
+
expect(result.exitCode).toBe(128);
|
|
273
|
+
expect(result.stderr).toContain('RPC failed');
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('error handling edge cases', () => {
|
|
278
|
+
it('should handle network failures', async () => {
|
|
279
|
+
mockFetch.mockRejectedValue(new Error('Network connection failed'));
|
|
280
|
+
|
|
281
|
+
await expect(client.checkout('https://github.com/user/repo.git', 'test-session'))
|
|
282
|
+
.rejects.toThrow('Network connection failed');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should handle malformed server responses', async () => {
|
|
286
|
+
mockFetch.mockResolvedValue(new Response('invalid json {', { status: 200 }));
|
|
287
|
+
|
|
288
|
+
await expect(client.checkout('https://github.com/user/repo.git', 'test-session'))
|
|
289
|
+
.rejects.toThrow(SandboxError);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should map server errors to client errors', async () => {
|
|
293
|
+
const serverErrorScenarios = [
|
|
294
|
+
{ status: 400, code: 'INVALID_GIT_URL', error: InvalidGitUrlError },
|
|
295
|
+
{ status: 401, code: 'GIT_AUTH_FAILED', error: GitAuthenticationError },
|
|
296
|
+
{ status: 404, code: 'GIT_REPOSITORY_NOT_FOUND', error: GitRepositoryNotFoundError },
|
|
297
|
+
{ status: 404, code: 'GIT_BRANCH_NOT_FOUND', error: GitBranchNotFoundError },
|
|
298
|
+
{ status: 500, code: 'GIT_OPERATION_FAILED', error: GitError },
|
|
299
|
+
{ status: 503, code: 'GIT_NETWORK_ERROR', error: GitNetworkError },
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
for (const scenario of serverErrorScenarios) {
|
|
303
|
+
mockFetch.mockResolvedValueOnce(new Response(
|
|
304
|
+
JSON.stringify({ error: 'Test error', code: scenario.code }),
|
|
305
|
+
{ status: scenario.status }
|
|
306
|
+
));
|
|
307
|
+
|
|
308
|
+
await expect(client.checkout('https://github.com/test/repo.git', 'test-session'))
|
|
309
|
+
.rejects.toThrow(scenario.error);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('constructor options', () => {
|
|
315
|
+
it('should initialize with minimal options', () => {
|
|
316
|
+
const minimalClient = new GitClient();
|
|
317
|
+
expect(minimalClient).toBeInstanceOf(GitClient);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should initialize with full options', () => {
|
|
321
|
+
const fullOptionsClient = new GitClient({
|
|
322
|
+
baseUrl: 'http://custom.com',
|
|
323
|
+
port: 8080,
|
|
324
|
+
});
|
|
325
|
+
expect(fullOptionsClient).toBeInstanceOf(GitClient);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
});
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
ExposePortResponse,
|
|
4
|
+
GetExposedPortsResponse,
|
|
5
|
+
UnexposePortResponse
|
|
6
|
+
} from '../src/clients';
|
|
7
|
+
import { PortClient } from '../src/clients/port-client';
|
|
8
|
+
import {
|
|
9
|
+
InvalidPortError,
|
|
10
|
+
PortAlreadyExposedError,
|
|
11
|
+
PortError,
|
|
12
|
+
PortInUseError,
|
|
13
|
+
PortNotExposedError,
|
|
14
|
+
SandboxError,
|
|
15
|
+
ServiceNotRespondingError
|
|
16
|
+
} from '../src/errors';
|
|
17
|
+
|
|
18
|
+
describe('PortClient', () => {
|
|
19
|
+
let client: PortClient;
|
|
20
|
+
let mockFetch: ReturnType<typeof vi.fn>;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
|
|
25
|
+
mockFetch = vi.fn();
|
|
26
|
+
global.fetch = mockFetch as unknown as typeof fetch;
|
|
27
|
+
|
|
28
|
+
client = new PortClient({
|
|
29
|
+
baseUrl: 'http://test.com',
|
|
30
|
+
port: 3000,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
vi.restoreAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('service exposure', () => {
|
|
39
|
+
it('should expose web services successfully', async () => {
|
|
40
|
+
const mockResponse: ExposePortResponse = {
|
|
41
|
+
success: true,
|
|
42
|
+
port: 3001,
|
|
43
|
+
exposedAt: 'https://preview-abc123.workers.dev',
|
|
44
|
+
name: 'web-server',
|
|
45
|
+
timestamp: '2023-01-01T00:00:00Z',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
mockFetch.mockResolvedValue(new Response(
|
|
49
|
+
JSON.stringify(mockResponse),
|
|
50
|
+
{ status: 200 }
|
|
51
|
+
));
|
|
52
|
+
|
|
53
|
+
const result = await client.exposePort(3001, 'session-123', 'web-server');
|
|
54
|
+
|
|
55
|
+
expect(result.success).toBe(true);
|
|
56
|
+
expect(result.port).toBe(3001);
|
|
57
|
+
expect(result.exposedAt).toBe('https://preview-abc123.workers.dev');
|
|
58
|
+
expect(result.name).toBe('web-server');
|
|
59
|
+
expect(result.exposedAt.startsWith('https://')).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should expose API services on different ports', async () => {
|
|
63
|
+
const mockResponse: ExposePortResponse = {
|
|
64
|
+
success: true,
|
|
65
|
+
port: 8080,
|
|
66
|
+
exposedAt: 'https://api-def456.workers.dev',
|
|
67
|
+
name: 'api-server',
|
|
68
|
+
timestamp: '2023-01-01T00:00:00Z',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
mockFetch.mockResolvedValue(new Response(
|
|
72
|
+
JSON.stringify(mockResponse),
|
|
73
|
+
{ status: 200 }
|
|
74
|
+
));
|
|
75
|
+
|
|
76
|
+
const result = await client.exposePort(8080, 'session-456', 'api-server');
|
|
77
|
+
|
|
78
|
+
expect(result.success).toBe(true);
|
|
79
|
+
expect(result.port).toBe(8080);
|
|
80
|
+
expect(result.name).toBe('api-server');
|
|
81
|
+
expect(result.exposedAt).toContain('api-');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should expose services without explicit names', async () => {
|
|
85
|
+
const mockResponse: ExposePortResponse = {
|
|
86
|
+
success: true,
|
|
87
|
+
port: 5000,
|
|
88
|
+
exposedAt: 'https://service-ghi789.workers.dev',
|
|
89
|
+
timestamp: '2023-01-01T00:00:00Z',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
mockFetch.mockResolvedValue(new Response(
|
|
93
|
+
JSON.stringify(mockResponse),
|
|
94
|
+
{ status: 200 }
|
|
95
|
+
));
|
|
96
|
+
|
|
97
|
+
const result = await client.exposePort(5000, 'session-789');
|
|
98
|
+
|
|
99
|
+
expect(result.success).toBe(true);
|
|
100
|
+
expect(result.port).toBe(5000);
|
|
101
|
+
expect(result.name).toBeUndefined();
|
|
102
|
+
expect(result.exposedAt).toBeDefined();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('service management', () => {
|
|
108
|
+
it('should list all exposed services', async () => {
|
|
109
|
+
const mockResponse: GetExposedPortsResponse = {
|
|
110
|
+
success: true,
|
|
111
|
+
ports: [
|
|
112
|
+
{
|
|
113
|
+
port: 3000,
|
|
114
|
+
exposedAt: 'https://frontend-abc123.workers.dev',
|
|
115
|
+
name: 'frontend',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
port: 4000,
|
|
119
|
+
exposedAt: 'https://api-def456.workers.dev',
|
|
120
|
+
name: 'api',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
port: 5432,
|
|
124
|
+
exposedAt: 'https://db-ghi789.workers.dev',
|
|
125
|
+
name: 'database',
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
count: 3,
|
|
129
|
+
timestamp: '2023-01-01T00:10:00Z',
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
mockFetch.mockResolvedValue(new Response(
|
|
133
|
+
JSON.stringify(mockResponse),
|
|
134
|
+
{ status: 200 }
|
|
135
|
+
));
|
|
136
|
+
|
|
137
|
+
const result = await client.getExposedPorts('session-list');
|
|
138
|
+
|
|
139
|
+
expect(result.success).toBe(true);
|
|
140
|
+
expect(result.count).toBe(3);
|
|
141
|
+
expect(result.ports).toHaveLength(3);
|
|
142
|
+
|
|
143
|
+
result.ports.forEach(service => {
|
|
144
|
+
expect(service.exposedAt).toContain('.workers.dev');
|
|
145
|
+
expect(service.port).toBeGreaterThan(0);
|
|
146
|
+
expect(service.name).toBeDefined();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should handle empty exposed ports list', async () => {
|
|
151
|
+
const mockResponse: GetExposedPortsResponse = {
|
|
152
|
+
success: true,
|
|
153
|
+
ports: [],
|
|
154
|
+
count: 0,
|
|
155
|
+
timestamp: '2023-01-01T00:00:00Z',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
mockFetch.mockResolvedValue(new Response(
|
|
159
|
+
JSON.stringify(mockResponse),
|
|
160
|
+
{ status: 200 }
|
|
161
|
+
));
|
|
162
|
+
|
|
163
|
+
const result = await client.getExposedPorts('session-empty');
|
|
164
|
+
|
|
165
|
+
expect(result.success).toBe(true);
|
|
166
|
+
expect(result.count).toBe(0);
|
|
167
|
+
expect(result.ports).toHaveLength(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should unexpose services cleanly', async () => {
|
|
171
|
+
const mockResponse: UnexposePortResponse = {
|
|
172
|
+
success: true,
|
|
173
|
+
port: 3001,
|
|
174
|
+
timestamp: '2023-01-01T00:15:00Z',
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
mockFetch.mockResolvedValue(new Response(
|
|
178
|
+
JSON.stringify(mockResponse),
|
|
179
|
+
{ status: 200 }
|
|
180
|
+
));
|
|
181
|
+
|
|
182
|
+
const result = await client.unexposePort(3001, 'session-unexpose');
|
|
183
|
+
|
|
184
|
+
expect(result.success).toBe(true);
|
|
185
|
+
expect(result.port).toBe(3001);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('port validation and error handling', () => {
|
|
191
|
+
it('should handle port already exposed errors', async () => {
|
|
192
|
+
const errorResponse = {
|
|
193
|
+
error: 'Port already exposed: 3000',
|
|
194
|
+
code: 'PORT_ALREADY_EXPOSED'
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
mockFetch.mockResolvedValue(new Response(
|
|
198
|
+
JSON.stringify(errorResponse),
|
|
199
|
+
{ status: 409 }
|
|
200
|
+
));
|
|
201
|
+
|
|
202
|
+
await expect(client.exposePort(3000, 'session-err'))
|
|
203
|
+
.rejects.toThrow(PortAlreadyExposedError);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should handle invalid port numbers', async () => {
|
|
207
|
+
const errorResponse = {
|
|
208
|
+
error: 'Invalid port number: 0',
|
|
209
|
+
code: 'INVALID_PORT_NUMBER'
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
mockFetch.mockResolvedValue(new Response(
|
|
213
|
+
JSON.stringify(errorResponse),
|
|
214
|
+
{ status: 400 }
|
|
215
|
+
));
|
|
216
|
+
|
|
217
|
+
await expect(client.exposePort(0, 'session-err'))
|
|
218
|
+
.rejects.toThrow(InvalidPortError);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should handle port in use errors', async () => {
|
|
222
|
+
const errorResponse = {
|
|
223
|
+
error: 'Port in use: 3000 is already bound by another process',
|
|
224
|
+
code: 'PORT_IN_USE'
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
mockFetch.mockResolvedValue(new Response(
|
|
228
|
+
JSON.stringify(errorResponse),
|
|
229
|
+
{ status: 409 }
|
|
230
|
+
));
|
|
231
|
+
|
|
232
|
+
await expect(client.exposePort(3000, 'session-err'))
|
|
233
|
+
.rejects.toThrow(PortInUseError);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should handle service not responding errors', async () => {
|
|
237
|
+
const errorResponse = {
|
|
238
|
+
error: 'Service not responding on port 8080',
|
|
239
|
+
code: 'SERVICE_NOT_RESPONDING'
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
mockFetch.mockResolvedValue(new Response(
|
|
243
|
+
JSON.stringify(errorResponse),
|
|
244
|
+
{ status: 503 }
|
|
245
|
+
));
|
|
246
|
+
|
|
247
|
+
await expect(client.exposePort(8080, 'session-err'))
|
|
248
|
+
.rejects.toThrow(ServiceNotRespondingError);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should handle unexpose non-existent port', async () => {
|
|
252
|
+
const errorResponse = {
|
|
253
|
+
error: 'Port not exposed: 9999',
|
|
254
|
+
code: 'PORT_NOT_EXPOSED'
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
mockFetch.mockResolvedValue(new Response(
|
|
258
|
+
JSON.stringify(errorResponse),
|
|
259
|
+
{ status: 404 }
|
|
260
|
+
));
|
|
261
|
+
|
|
262
|
+
await expect(client.unexposePort(9999, 'session-err'))
|
|
263
|
+
.rejects.toThrow(PortNotExposedError);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should handle port operation failures', async () => {
|
|
267
|
+
const errorResponse = {
|
|
268
|
+
error: 'Port operation failed: unable to setup proxy',
|
|
269
|
+
code: 'PORT_OPERATION_ERROR'
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
mockFetch.mockResolvedValue(new Response(
|
|
273
|
+
JSON.stringify(errorResponse),
|
|
274
|
+
{ status: 500 }
|
|
275
|
+
));
|
|
276
|
+
|
|
277
|
+
await expect(client.exposePort(3000, 'session-err'))
|
|
278
|
+
.rejects.toThrow(PortError);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('edge cases and resilience', () => {
|
|
283
|
+
it('should handle network failures gracefully', async () => {
|
|
284
|
+
mockFetch.mockRejectedValue(new Error('Network connection failed'));
|
|
285
|
+
|
|
286
|
+
await expect(client.exposePort(3000, 'session-net'))
|
|
287
|
+
.rejects.toThrow('Network connection failed');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should handle malformed server responses', async () => {
|
|
291
|
+
mockFetch.mockResolvedValue(new Response(
|
|
292
|
+
'invalid json {',
|
|
293
|
+
{ status: 200 }
|
|
294
|
+
));
|
|
295
|
+
|
|
296
|
+
await expect(client.exposePort(3000, 'session-malform'))
|
|
297
|
+
.rejects.toThrow(SandboxError);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
});
|
|
301
|
+
});
|