diff --git a/src/Sandbox.js b/src/Sandbox.js index af6420a..0dab58b 100644 --- a/src/Sandbox.js +++ b/src/Sandbox.js @@ -93,7 +93,7 @@ class Sandbox { if (options.policy !== undefined) body.policy = options.policy if (options.ports !== undefined) body.ports = options.ports - const url = `${creds.apiHost}/api/v1/namespaces/${creds.namespace}/sandbox` + const url = `${creds.apiHost}/api/v1/namespaces/${creds.namespace}/sandboxes` const payload = await apiRequest('POST', url, creds.apiKey, body) const sandboxId = payload.sandboxId @@ -134,7 +134,7 @@ class Sandbox { static async get(sandboxId, options = {}) { console.warn('[aio-lib-sandbox] alpha — APIs may change without notice') const creds = resolveCredentials(options) - const url = `${creds.apiHost}/api/v1/namespaces/${creds.namespace}/sandbox/${sandboxId}` + const url = `${creds.apiHost}/api/v1/namespaces/${creds.namespace}/sandboxes/${sandboxId}` const payload = await apiRequest('GET', url, creds.apiKey) return new Sandbox({ @@ -493,7 +493,7 @@ class Sandbox { */ async destroy() { const base = this.managementEndpoint || this.apiHost - const url = `${base}/api/v1/namespaces/${this.namespace}/sandbox/${this.id}` + const url = `${base}/api/v1/namespaces/${this.namespace}/sandboxes/${this.id}` this.ws?.beginIntentionalClose() let payload diff --git a/src/utils.js b/src/utils.js index b8abc97..bdb9f86 100644 --- a/src/utils.js +++ b/src/utils.js @@ -45,7 +45,7 @@ function normalizeApiHost (host) { function buildWebSocketEndpoint (apiHost, namespace, sandboxId) { const url = new URL(apiHost) url.protocol = url.protocol === 'http:' ? 'ws:' : 'wss:' - url.pathname = `/ws/v1/namespaces/${namespace}/sandbox/${sandboxId}/exec` + url.pathname = `/api/v1/namespaces/${namespace}/sandboxes/${sandboxId}/exec` url.search = '' return url.toString() } diff --git a/test/Sandbox.test.js b/test/Sandbox.test.js index daee0a3..4a7c152 100644 --- a/test/Sandbox.test.js +++ b/test/Sandbox.test.js @@ -67,7 +67,7 @@ class FakeWebSocket extends EventEmitter { const BASE_OPTIONS = { id: 'sb-test', - endpoint: 'wss://runtime.example.net/ws/v1/namespaces/ns/sandbox/sb-test/exec', + endpoint: 'wss://runtime.example.net/api/v1/namespaces/ns/sandboxes/sb-test/exec', status: 'ready', namespace: 'ns', apiHost: 'https://runtime.example.net', @@ -200,7 +200,7 @@ describe('Sandbox', () => { ok: true, json: () => Promise.resolve({ sandboxId: 'sb-new', - wsEndpoint: 'wss://runtime.example.net/ws/v1/namespaces/ns/sandbox/sb-new/exec', + wsEndpoint: 'wss://runtime.example.net/api/v1/namespaces/ns/sandboxes/sb-new/exec', status: 'ready', token: 'tok-new', maxLifetime: 3600, @@ -231,7 +231,7 @@ describe('Sandbox', () => { [3000, 'https://sb-new-3000.preview.example.net'] ])) expect(mockFetch).toHaveBeenCalledWith( - 'https://runtime.example.net/api/v1/namespaces/ns/sandbox', + 'https://runtime.example.net/api/v1/namespaces/ns/sandboxes', expect.objectContaining({ method: 'POST' }) ) }) @@ -241,7 +241,7 @@ describe('Sandbox', () => { ok: true, json: () => Promise.resolve({ sandboxId: 'sb-pol', - wsEndpoint: 'wss://runtime.example.net/ws/v1/namespaces/ns/sandbox/sb-pol/exec', + wsEndpoint: 'wss://runtime.example.net/api/v1/namespaces/ns/sandboxes/sb-pol/exec', status: 'ready', token: 'tok-pol', maxLifetime: 3600 @@ -272,7 +272,7 @@ describe('Sandbox', () => { ok: true, json: () => Promise.resolve({ sandboxId: 'sb-ports', - wsEndpoint: 'wss://runtime.example.net/ws/v1/namespaces/ns/sandbox/sb-ports/exec', + wsEndpoint: 'wss://runtime.example.net/api/v1/namespaces/ns/sandboxes/sb-ports/exec', status: 'ready', token: 'tok-ports', maxLifetime: 3600, @@ -312,7 +312,7 @@ describe('Sandbox', () => { ok: true, json: () => Promise.resolve({ sandboxId: 'sb-env', - wsEndpoint: 'wss://runtime.example.net/ws/v1/namespaces/ns/sandbox/sb-env/exec', + wsEndpoint: 'wss://runtime.example.net/api/v1/namespaces/ns/sandboxes/sb-env/exec', status: 'ready', token: 'tok-env', maxLifetime: 3600 @@ -408,6 +408,38 @@ describe('Sandbox', () => { Sandbox.get('sb-x', { apiHost: 'https://runtime.example.net', namespace: 'ns', auth: 'bad' }) ).rejects.toThrow(SandboxUnauthorizedError) }) + + test('throws SandboxTimeoutError on 504', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 504, + text: () => Promise.resolve('gateway timeout') + }) + + await expect( + Sandbox.get('sb-slow', { apiHost: 'https://runtime.example.net', namespace: 'ns', auth: 'key' }) + ).rejects.toThrow(SandboxTimeoutError) + }) + + test('throws SandboxClientError on unexpected API status', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 500, + text: () => Promise.resolve('server error') + }) + + await expect( + Sandbox.get('sb-error', { apiHost: 'https://runtime.example.net', namespace: 'ns', auth: 'key' }) + ).rejects.toThrow(SandboxClientError) + }) + + test('wraps fetch failures in SandboxClientError', async () => { + global.fetch = jest.fn().mockRejectedValue(new Error('network unavailable')) + + await expect( + Sandbox.get('sb-network', { apiHost: 'https://runtime.example.net', namespace: 'ns', auth: 'key' }) + ).rejects.toThrow(SandboxClientError) + }) }) // ------------------------------------------------------------------------- @@ -844,7 +876,7 @@ describe('Sandbox', () => { expect(result.status).toBe('destroyed') expect(sandbox.status).toBe('destroyed') expect(mockFetch).toHaveBeenCalledWith( - expect.stringContaining('/sandbox/sb-test'), + expect.stringContaining('/sandboxes/sb-test'), expect.objectContaining({ method: 'DELETE' }) ) })