diff --git a/.changeset/regional-egress-repro.md b/.changeset/regional-egress-repro.md new file mode 100644 index 0000000..3847092 --- /dev/null +++ b/.changeset/regional-egress-repro.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/containers": patch +--- + +Add deployed egress interception tests covering global and regional Worker placement. diff --git a/examples/egress-tests/test/egress.test.ts b/examples/egress-tests/test/egress.test.ts index ede331a..39d6062 100644 --- a/examples/egress-tests/test/egress.test.ts +++ b/examples/egress-tests/test/egress.test.ts @@ -1,6 +1,5 @@ import { randomUUID } from 'node:crypto'; -import { describe, expect, vi } from 'vitest'; -import { test, WranglerDevRunner } from '../../test-helpers'; +import { describe, expect, test, vi } from 'vitest'; /** * Egress interception tests. @@ -14,142 +13,145 @@ import { test, WranglerDevRunner } from '../../test-helpers'; * outbound = catch-all handler */ describe('egress interception', () => { - describe('local', () => { - async function proxyVia( - runner: WranglerDevRunner, - id: string, - target: string - ): Promise { - const url = await runner.getUrl(); - return vi.waitFor( - async () => { - const res = await fetch(`${url}/proxy?id=${id}&proxy=${encodeURIComponent(target)}`); - if (res.status === 500 || res.status === 503) { - throw new Error(`Container not ready, got ${res.status}`); - } - return res; - }, - { timeout: 15000 } - ); + const baseUrl = process.env.EGRESS_TEST_BASE_URL; + + if (!baseUrl) { + throw new Error('EGRESS_TEST_BASE_URL must be set to the deployed Worker URL.'); + } + + describe(process.env.EGRESS_TEST_ENV ?? 'deployed', () => { + async function proxyVia(id: string, target: string): Promise { + let lastResponse = ''; + try { + return await vi.waitFor( + async () => { + const res = await fetch(`${baseUrl}/proxy?id=${id}&proxy=${encodeURIComponent(target)}`); + if (res.status === 500 || res.status === 503) { + lastResponse = await res.text(); + throw new Error(`Container not ready, got ${res.status}: ${lastResponse}`); + } + return res; + }, + { timeout: 120000 } + ); + } catch (error) { + if (lastResponse) { + throw new Error(`${error instanceof Error ? error.message : String(error)} +Last response: ${lastResponse}`); + } + throw error; + } } - async function destroyContainer(runner: WranglerDevRunner, id: string) { - // Tell the worker to destroy the container so the in-container onStop - // hook can fire before the fixture tears down wrangler dev itself. - // The full wrangler+workerd cleanup is handled automatically by the - // `runner` test fixture. - const url = await runner.getUrl(); - await fetch(`${url}/destroy?id=${id}`); + async function destroyContainer(id: string) { + await fetch(`${baseUrl}/destroy?id=${id}`); await new Promise(resolve => setTimeout(resolve, 1000)); } - async function denyHost(runner: WranglerDevRunner, id: string, hostname: string) { - const url = await runner.getUrl(); + async function denyHost(id: string, hostname: string) { const res = await fetch( - `${url}/config/deny-host?id=${id}&hostname=${encodeURIComponent(hostname)}` + `${baseUrl}/config/deny-host?id=${id}&hostname=${encodeURIComponent(hostname)}` ); expect(res.status).toBe(200); } - test('deniedHosts blocks the request', async ({ runner }) => { + test('deniedHosts blocks the request', async () => { const id = randomUUID(); - const res = await proxyVia(runner, id, 'denied.com'); + const res = await proxyVia(id, 'denied.com'); expect(res.status).toBe(520); const body = await res.text(); expect(body).toContain('Origin is disallowed'); - await destroyContainer(runner, id); + await destroyContainer(id); }); - test('allowedHosts gate blocks non-allowed hosts', async ({ runner }) => { + test('allowedHosts gate blocks non-allowed hosts', async () => { const id = randomUUID(); - const res = await proxyVia(runner, id, 'random.com'); + const res = await proxyVia(id, 'random.com'); expect(res.status).toBe(520); const body = await res.text(); expect(body).toContain('Origin is disallowed'); - await destroyContainer(runner, id); + await destroyContainer(id); }); - test('outboundByHost handler is invoked for matching allowed host', async ({ runner }) => { + test('outboundByHost handler is invoked for matching allowed host', async () => { const id = randomUUID(); - const res = await proxyVia(runner, id, 'by-host.com'); + const res = await proxyVia(id, 'by-host.com'); expect(res.status).toBe(200); const body = await res.text(); expect(body).toBe('outboundByHost: by-host.com'); - await destroyContainer(runner, id); + await destroyContainer(id); }); - test('catch-all outbound handler is invoked for allowed host without specific handler', async ({ - runner, - }) => { + test('catch-all outbound handler is invoked for allowed host without specific handler', async () => { const id = randomUUID(); - const res = await proxyVia(runner, id, 'allowed.com'); + const res = await proxyVia(id, 'allowed.com'); expect(res.status).toBe(200); const body = await res.text(); expect(body).toBe('catch-all: allowed.com'); - await destroyContainer(runner, id); + await destroyContainer(id); }); - test('denied host is blocked even if it would match allowedHosts', async ({ runner }) => { + test('denied host is blocked even if it would match allowedHosts', async () => { const id = randomUUID(); - const res = await proxyVia(runner, id, 'denied.com'); + const res = await proxyVia(id, 'denied.com'); expect(res.status).toBe(520); - await destroyContainer(runner, id); + await destroyContainer(id); }); - test('glob pattern in outboundByHost matches subdomains', async ({ runner }) => { + test('glob pattern in outboundByHost matches subdomains', async () => { const id = randomUUID(); - const res = await proxyVia(runner, id, 'api.globtest.com'); + const res = await proxyVia(id, 'api.globtest.com'); expect(res.status).toBe(200); const body = await res.text(); expect(body).toBe('outboundByHost glob: api.globtest.com'); - await destroyContainer(runner, id); + await destroyContainer(id); }); - test('glob pattern in outboundByHost matches deeply nested subdomains', async ({ runner }) => { + test('glob pattern in outboundByHost matches deeply nested subdomains', async () => { const id = randomUUID(); - const res = await proxyVia(runner, id, 'a.b.globtest.com'); + const res = await proxyVia(id, 'a.b.globtest.com'); expect(res.status).toBe(200); const body = await res.text(); expect(body).toBe('outboundByHost glob: a.b.globtest.com'); - await destroyContainer(runner, id); + await destroyContainer(id); }); - test('glob pattern in allowedHosts blocks non-matching host', async ({ runner }) => { + test('glob pattern in allowedHosts blocks non-matching host', async () => { const id = randomUUID(); // globtest.com itself does NOT match *.globtest.com - const res = await proxyVia(runner, id, 'globtest.com'); + const res = await proxyVia(id, 'globtest.com'); expect(res.status).toBe(520); - await destroyContainer(runner, id); + await destroyContainer(id); }); - test('denyHost also blocks the same hostname with a trailing dot', async ({ runner }) => { + test('denyHost also blocks the same hostname with a trailing dot', async () => { const id = randomUUID(); const hostname = `allowed-${randomUUID()}.example.com`; - await denyHost(runner, id, hostname); + await denyHost(id, hostname); - const res = await proxyVia(runner, id, `${hostname}.`); + const res = await proxyVia(id, `${hostname}.`); expect(res.status).toBe(520); const body = await res.text(); expect(body).toContain('Origin is disallowed'); - await destroyContainer(runner, id); + await destroyContainer(id); }); }); }); diff --git a/examples/egress-tests/vitest.config.ts b/examples/egress-tests/vitest.config.ts index df9a49c..d0d9fdd 100644 --- a/examples/egress-tests/vitest.config.ts +++ b/examples/egress-tests/vitest.config.ts @@ -2,6 +2,6 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - testTimeout: 30000, + testTimeout: 180000, }, }); diff --git a/examples/egress-tests/wrangler.jsonc b/examples/egress-tests/wrangler.jsonc index 010df32..f2e58a0 100644 --- a/examples/egress-tests/wrangler.jsonc +++ b/examples/egress-tests/wrangler.jsonc @@ -2,6 +2,7 @@ "name": "egress-tests", "main": "src/index.ts", "compatibility_date": "2026-05-12", + "workers_dev": true, "observability": { "enabled": true, }, @@ -10,7 +11,7 @@ "image": "./Dockerfile", "class_name": "EgressTestContainer", "name": "egress-test-container", - "max_instances": 2, + "max_instances": 12, }, ], "durable_objects": { @@ -27,4 +28,58 @@ "new_sqlite_classes": ["EgressTestContainer"], }, ], + "env": { + "global": { + "containers": [ + { + "image": "./Dockerfile", + "class_name": "EgressTestContainer", + "name": "egress-test-container-global", + "max_instances": 12, + }, + ], + "durable_objects": { + "bindings": [ + { + "class_name": "EgressTestContainer", + "name": "CONTAINER", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["EgressTestContainer"], + }, + ], + }, + "regional": { + "placement": { + "mode": "targeted", + "region": "aws:us-east-1", + }, + "containers": [ + { + "image": "./Dockerfile", + "class_name": "EgressTestContainer", + "name": "egress-test-container-regional", + "max_instances": 12, + }, + ], + "durable_objects": { + "bindings": [ + { + "class_name": "EgressTestContainer", + "name": "CONTAINER", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["EgressTestContainer"], + }, + ], + }, + }, }