@cloudflare/sandbox 0.0.0-104f455 → 0.0.0-153e416

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.
@@ -0,0 +1,240 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { proxyToSandbox, type SandboxEnv } from '../src/request-handler';
3
+ import type { Sandbox } from '../src/sandbox';
4
+
5
+ // Mock getSandbox from sandbox.ts
6
+ vi.mock('../src/sandbox', () => {
7
+ const mockFn = vi.fn();
8
+ return {
9
+ getSandbox: mockFn,
10
+ Sandbox: vi.fn(),
11
+ };
12
+ });
13
+
14
+ // Import the mock after vi.mock is set up
15
+ import { getSandbox } from '../src/sandbox';
16
+
17
+ describe('proxyToSandbox - WebSocket Support', () => {
18
+ let mockSandbox: Partial<Sandbox>;
19
+ let mockEnv: SandboxEnv;
20
+
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+
24
+ // Mock Sandbox with necessary methods
25
+ mockSandbox = {
26
+ validatePortToken: vi.fn().mockResolvedValue(true),
27
+ fetch: vi.fn().mockResolvedValue(new Response('WebSocket response')),
28
+ containerFetch: vi.fn().mockResolvedValue(new Response('HTTP response')),
29
+ };
30
+
31
+ mockEnv = {
32
+ Sandbox: {} as any,
33
+ };
34
+
35
+ vi.mocked(getSandbox).mockReturnValue(mockSandbox as Sandbox);
36
+ });
37
+
38
+ describe('WebSocket detection and routing', () => {
39
+ it('should detect WebSocket upgrade header (case-insensitive)', async () => {
40
+ const request = new Request('https://8080-sandbox-token12345678901.example.com/ws', {
41
+ headers: {
42
+ 'Upgrade': 'websocket',
43
+ 'Connection': 'Upgrade',
44
+ },
45
+ });
46
+
47
+ await proxyToSandbox(request, mockEnv);
48
+
49
+ // Should route through fetch() for WebSocket
50
+ expect(mockSandbox.fetch).toHaveBeenCalledTimes(1);
51
+ expect(mockSandbox.containerFetch).not.toHaveBeenCalled();
52
+ });
53
+
54
+ it('should set cf-container-target-port header for WebSocket', async () => {
55
+ const request = new Request('https://8080-sandbox-token12345678901.example.com/ws', {
56
+ headers: {
57
+ 'Upgrade': 'websocket',
58
+ },
59
+ });
60
+
61
+ await proxyToSandbox(request, mockEnv);
62
+
63
+ expect(mockSandbox.fetch).toHaveBeenCalledTimes(1);
64
+ const fetchCall = vi.mocked(mockSandbox.fetch as any).mock.calls[0][0] as Request;
65
+ expect(fetchCall.headers.get('cf-container-target-port')).toBe('8080');
66
+ });
67
+
68
+ it('should preserve original headers for WebSocket', async () => {
69
+ const request = new Request('https://8080-sandbox-token12345678901.example.com/ws', {
70
+ headers: {
71
+ 'Upgrade': 'websocket',
72
+ 'Sec-WebSocket-Key': 'test-key-123',
73
+ 'Sec-WebSocket-Version': '13',
74
+ 'User-Agent': 'test-client',
75
+ },
76
+ });
77
+
78
+ await proxyToSandbox(request, mockEnv);
79
+
80
+ const fetchCall = vi.mocked(mockSandbox.fetch as any).mock.calls[0][0] as Request;
81
+ expect(fetchCall.headers.get('Upgrade')).toBe('websocket');
82
+ expect(fetchCall.headers.get('Sec-WebSocket-Key')).toBe('test-key-123');
83
+ expect(fetchCall.headers.get('Sec-WebSocket-Version')).toBe('13');
84
+ expect(fetchCall.headers.get('User-Agent')).toBe('test-client');
85
+ });
86
+ });
87
+
88
+ describe('HTTP routing (existing behavior)', () => {
89
+ it('should route HTTP requests through containerFetch', async () => {
90
+ const request = new Request('https://8080-sandbox-token12345678901.example.com/api/data', {
91
+ method: 'GET',
92
+ });
93
+
94
+ await proxyToSandbox(request, mockEnv);
95
+
96
+ // Should route through containerFetch() for HTTP
97
+ expect(mockSandbox.containerFetch).toHaveBeenCalledTimes(1);
98
+ expect(mockSandbox.fetch).not.toHaveBeenCalled();
99
+ });
100
+
101
+ it('should route POST requests through containerFetch', async () => {
102
+ const request = new Request('https://8080-sandbox-token12345678901.example.com/api/data', {
103
+ method: 'POST',
104
+ body: JSON.stringify({ data: 'test' }),
105
+ headers: {
106
+ 'Content-Type': 'application/json',
107
+ },
108
+ });
109
+
110
+ await proxyToSandbox(request, mockEnv);
111
+
112
+ expect(mockSandbox.containerFetch).toHaveBeenCalledTimes(1);
113
+ expect(mockSandbox.fetch).not.toHaveBeenCalled();
114
+ });
115
+
116
+ it('should not detect SSE as WebSocket', async () => {
117
+ const request = new Request('https://8080-sandbox-token12345678901.example.com/events', {
118
+ headers: {
119
+ 'Accept': 'text/event-stream',
120
+ },
121
+ });
122
+
123
+ await proxyToSandbox(request, mockEnv);
124
+
125
+ // SSE should use HTTP path, not WebSocket path
126
+ expect(mockSandbox.containerFetch).toHaveBeenCalledTimes(1);
127
+ expect(mockSandbox.fetch).not.toHaveBeenCalled();
128
+ });
129
+ });
130
+
131
+ describe('Token validation', () => {
132
+ it('should validate token for both WebSocket and HTTP requests', async () => {
133
+ const wsRequest = new Request('https://8080-sandbox-token12345678901.example.com/ws', {
134
+ headers: { 'Upgrade': 'websocket' },
135
+ });
136
+
137
+ await proxyToSandbox(wsRequest, mockEnv);
138
+ expect(mockSandbox.validatePortToken).toHaveBeenCalledWith(8080, 'token12345678901');
139
+
140
+ vi.clearAllMocks();
141
+
142
+ const httpRequest = new Request('https://8080-sandbox-token12345678901.example.com/api');
143
+ await proxyToSandbox(httpRequest, mockEnv);
144
+ expect(mockSandbox.validatePortToken).toHaveBeenCalledWith(8080, 'token12345678901');
145
+ });
146
+
147
+ it('should reject requests with invalid token', async () => {
148
+ vi.mocked(mockSandbox.validatePortToken as any).mockResolvedValue(false);
149
+
150
+ const request = new Request('https://8080-sandbox-invalidtoken1234.example.com/ws', {
151
+ headers: { 'Upgrade': 'websocket' },
152
+ });
153
+
154
+ const response = await proxyToSandbox(request, mockEnv);
155
+
156
+ expect(response?.status).toBe(404);
157
+ expect(mockSandbox.fetch).not.toHaveBeenCalled();
158
+
159
+ const body = await response?.json();
160
+ expect(body).toMatchObject({
161
+ error: 'Access denied: Invalid token or port not exposed',
162
+ code: 'INVALID_TOKEN',
163
+ });
164
+ });
165
+
166
+ it('should reject reserved port 3000', async () => {
167
+ // Port 3000 is reserved as control plane port and rejected by validatePort()
168
+ const request = new Request('https://3000-sandbox-anytoken12345678.example.com/status', {
169
+ method: 'GET',
170
+ });
171
+
172
+ const response = await proxyToSandbox(request, mockEnv);
173
+
174
+ // Port 3000 is reserved and should be rejected (extractSandboxRoute returns null)
175
+ expect(response).toBeNull();
176
+ expect(mockSandbox.validatePortToken).not.toHaveBeenCalled();
177
+ expect(mockSandbox.containerFetch).not.toHaveBeenCalled();
178
+ });
179
+ });
180
+
181
+ describe('Port routing', () => {
182
+ it('should route to correct port from subdomain', async () => {
183
+ const request = new Request('https://9000-sandbox-token12345678901.example.com/api', {
184
+ method: 'GET',
185
+ });
186
+
187
+ await proxyToSandbox(request, mockEnv);
188
+
189
+ expect(mockSandbox.validatePortToken).toHaveBeenCalledWith(9000, 'token12345678901');
190
+ });
191
+ });
192
+
193
+ describe('Non-sandbox requests', () => {
194
+ it('should return null for non-sandbox URLs', async () => {
195
+ const request = new Request('https://example.com/some-path');
196
+
197
+ const response = await proxyToSandbox(request, mockEnv);
198
+
199
+ expect(response).toBeNull();
200
+ expect(mockSandbox.fetch).not.toHaveBeenCalled();
201
+ expect(mockSandbox.containerFetch).not.toHaveBeenCalled();
202
+ });
203
+
204
+ it('should return null for invalid subdomain patterns', async () => {
205
+ const request = new Request('https://invalid-pattern.example.com');
206
+
207
+ const response = await proxyToSandbox(request, mockEnv);
208
+
209
+ expect(response).toBeNull();
210
+ });
211
+ });
212
+
213
+ describe('Error handling', () => {
214
+ it('should handle errors during WebSocket routing', async () => {
215
+ (mockSandbox.fetch as any).mockImplementation(() => Promise.reject(new Error('Connection failed')));
216
+
217
+ const request = new Request('https://8080-sandbox-token12345678901.example.com/ws', {
218
+ headers: {
219
+ 'Upgrade': 'websocket',
220
+ },
221
+ });
222
+
223
+ const response = await proxyToSandbox(request, mockEnv);
224
+
225
+ expect(response?.status).toBe(500);
226
+ const text = await response?.text();
227
+ expect(text).toBe('Proxy routing error');
228
+ });
229
+
230
+ it('should handle errors during HTTP routing', async () => {
231
+ (mockSandbox.containerFetch as any).mockImplementation(() => Promise.reject(new Error('Service error')));
232
+
233
+ const request = new Request('https://8080-sandbox-token12345678901.example.com/api');
234
+
235
+ const response = await proxyToSandbox(request, mockEnv);
236
+
237
+ expect(response?.status).toBe(500);
238
+ });
239
+ });
240
+ });
@@ -1,23 +1,36 @@
1
1
  import type { DurableObjectState } from '@cloudflare/workers-types';
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
  import { Sandbox } from '../src/sandbox';
4
+ import { Container } from '@cloudflare/containers';
4
5
 
5
6
  // Mock dependencies before imports
6
7
  vi.mock('./interpreter', () => ({
7
8
  CodeInterpreter: vi.fn().mockImplementation(() => ({})),
8
9
  }));
9
10
 
10
- vi.mock('@cloudflare/containers', () => ({
11
- Container: class Container {
11
+ vi.mock('@cloudflare/containers', () => {
12
+ const MockContainer = class Container {
12
13
  ctx: any;
13
14
  env: any;
14
15
  constructor(ctx: any, env: any) {
15
16
  this.ctx = ctx;
16
17
  this.env = env;
17
18
  }
18
- },
19
- getContainer: vi.fn(),
20
- }));
19
+ async fetch(request: Request): Promise<Response> {
20
+ // Mock implementation - will be spied on in tests
21
+ return new Response('Mock Container fetch');
22
+ }
23
+ async containerFetch(request: Request, port: number): Promise<Response> {
24
+ // Mock implementation for HTTP path
25
+ return new Response('Mock Container HTTP fetch');
26
+ }
27
+ };
28
+
29
+ return {
30
+ Container: MockContainer,
31
+ getContainer: vi.fn(),
32
+ };
33
+ });
21
34
 
22
35
  describe('Sandbox - Automatic Session Management', () => {
23
36
  let sandbox: Sandbox;
@@ -462,4 +475,80 @@ describe('Sandbox - Automatic Session Management', () => {
462
475
  expect(sandbox.client.ports.exposePort).toHaveBeenCalled();
463
476
  });
464
477
  });
478
+
479
+ describe('fetch() override - WebSocket detection', () => {
480
+ let superFetchSpy: any;
481
+
482
+ beforeEach(async () => {
483
+ await sandbox.setSandboxName('test-sandbox');
484
+
485
+ // Spy on Container.prototype.fetch to verify WebSocket routing
486
+ superFetchSpy = vi.spyOn(Container.prototype, 'fetch')
487
+ .mockResolvedValue(new Response('WebSocket response'));
488
+ });
489
+
490
+ afterEach(() => {
491
+ superFetchSpy?.mockRestore();
492
+ });
493
+
494
+ it('should detect WebSocket upgrade header and route to super.fetch', async () => {
495
+ const request = new Request('https://example.com/ws', {
496
+ headers: {
497
+ 'Upgrade': 'websocket',
498
+ 'Connection': 'Upgrade',
499
+ },
500
+ });
501
+
502
+ const response = await sandbox.fetch(request);
503
+
504
+ // Should route through super.fetch() for WebSocket
505
+ expect(superFetchSpy).toHaveBeenCalledTimes(1);
506
+ expect(await response.text()).toBe('WebSocket response');
507
+ });
508
+
509
+ it('should route non-WebSocket requests through containerFetch', async () => {
510
+ // GET request
511
+ const getRequest = new Request('https://example.com/api/data');
512
+ await sandbox.fetch(getRequest);
513
+ expect(superFetchSpy).not.toHaveBeenCalled();
514
+
515
+ vi.clearAllMocks();
516
+
517
+ // POST request
518
+ const postRequest = new Request('https://example.com/api/data', {
519
+ method: 'POST',
520
+ body: JSON.stringify({ data: 'test' }),
521
+ headers: { 'Content-Type': 'application/json' },
522
+ });
523
+ await sandbox.fetch(postRequest);
524
+ expect(superFetchSpy).not.toHaveBeenCalled();
525
+
526
+ vi.clearAllMocks();
527
+
528
+ // SSE request (should not be detected as WebSocket)
529
+ const sseRequest = new Request('https://example.com/events', {
530
+ headers: { 'Accept': 'text/event-stream' },
531
+ });
532
+ await sandbox.fetch(sseRequest);
533
+ expect(superFetchSpy).not.toHaveBeenCalled();
534
+ });
535
+
536
+ it('should preserve WebSocket request unchanged when calling super.fetch()', async () => {
537
+ const request = new Request('https://example.com/ws', {
538
+ headers: {
539
+ 'Upgrade': 'websocket',
540
+ 'Sec-WebSocket-Key': 'test-key-123',
541
+ 'Sec-WebSocket-Version': '13',
542
+ },
543
+ });
544
+
545
+ await sandbox.fetch(request);
546
+
547
+ expect(superFetchSpy).toHaveBeenCalledTimes(1);
548
+ const passedRequest = superFetchSpy.mock.calls[0][0] as Request;
549
+ expect(passedRequest.headers.get('Upgrade')).toBe('websocket');
550
+ expect(passedRequest.headers.get('Sec-WebSocket-Key')).toBe('test-key-123');
551
+ expect(passedRequest.headers.get('Sec-WebSocket-Version')).toBe('13');
552
+ });
553
+ });
465
554
  });
@@ -1,7 +1,8 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import type {
3
3
  CommandsResponse,
4
- PingResponse
4
+ PingResponse,
5
+ VersionResponse
5
6
  } from '../src/clients';
6
7
  import { UtilityClient } from '../src/clients/utility-client';
7
8
  import {
@@ -25,6 +26,13 @@ const mockCommandsResponse = (commands: string[], overrides: Partial<CommandsRes
25
26
  ...overrides
26
27
  });
27
28
 
29
+ const mockVersionResponse = (version: string = '0.4.5', overrides: Partial<VersionResponse> = {}): VersionResponse => ({
30
+ success: true,
31
+ version,
32
+ timestamp: '2023-01-01T00:00:00Z',
33
+ ...overrides
34
+ });
35
+
28
36
  describe('UtilityClient', () => {
29
37
  let client: UtilityClient;
30
38
  let mockFetch: ReturnType<typeof vi.fn>;
@@ -249,6 +257,64 @@ describe('UtilityClient', () => {
249
257
  });
250
258
  });
251
259
 
260
+ describe('version checking', () => {
261
+ it('should get container version successfully', async () => {
262
+ mockFetch.mockResolvedValue(new Response(
263
+ JSON.stringify(mockVersionResponse('0.4.5')),
264
+ { status: 200 }
265
+ ));
266
+
267
+ const result = await client.getVersion();
268
+
269
+ expect(result).toBe('0.4.5');
270
+ });
271
+
272
+ it('should handle different version formats', async () => {
273
+ const versions = ['1.0.0', '2.5.3-beta', '0.0.1', '10.20.30'];
274
+
275
+ for (const version of versions) {
276
+ mockFetch.mockResolvedValueOnce(new Response(
277
+ JSON.stringify(mockVersionResponse(version)),
278
+ { status: 200 }
279
+ ));
280
+
281
+ const result = await client.getVersion();
282
+ expect(result).toBe(version);
283
+ }
284
+ });
285
+
286
+ it('should return "unknown" when version endpoint does not exist (backward compatibility)', async () => {
287
+ // Simulate 404 or other error for old containers
288
+ mockFetch.mockResolvedValue(new Response(
289
+ JSON.stringify({ error: 'Not Found' }),
290
+ { status: 404 }
291
+ ));
292
+
293
+ const result = await client.getVersion();
294
+
295
+ expect(result).toBe('unknown');
296
+ });
297
+
298
+ it('should return "unknown" on network failure (backward compatibility)', async () => {
299
+ mockFetch.mockRejectedValue(new Error('Network connection failed'));
300
+
301
+ const result = await client.getVersion();
302
+
303
+ expect(result).toBe('unknown');
304
+ });
305
+
306
+ it('should handle version response with unknown value', async () => {
307
+ mockFetch.mockResolvedValue(new Response(
308
+ JSON.stringify(mockVersionResponse('unknown')),
309
+ { status: 200 }
310
+ ));
311
+
312
+ const result = await client.getVersion();
313
+
314
+ expect(result).toBe('unknown');
315
+ });
316
+ });
317
+
252
318
  describe('constructor options', () => {
253
319
  it('should initialize with minimal options', () => {
254
320
  const minimalClient = new UtilityClient();
@@ -0,0 +1,16 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import packageJson from '../package.json';
3
+ import { SDK_VERSION } from '../src/version';
4
+
5
+ describe('Version Sync', () => {
6
+ test('SDK_VERSION matches package.json version', () => {
7
+ // Verify versions match
8
+ expect(SDK_VERSION).toBe(packageJson.version);
9
+ });
10
+
11
+ test('SDK_VERSION is a valid semver version', () => {
12
+ // Check if version matches semver pattern (major.minor.patch)
13
+ const semverPattern = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
14
+ expect(SDK_VERSION).toMatch(semverPattern);
15
+ });
16
+ });