Skip to content

Commit 2fff275

Browse files
JPeer264claude
andauthored
fixup! test(cloudflare): Add Flue E2E test that deploys a real Worker and sends to Sentry
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e49c565 commit 2fff275

5 files changed

Lines changed: 46 additions & 21 deletions

File tree

‎dev-packages/e2e-tests/README.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,24 @@ Make sure to add a `test:build` and `test:assert` command to the new app's `pack
116116
Sentry packages are automatically resolved to the local build via pnpm overrides injected at test time, so no manual
117117
registry configuration is needed.
118118

119+
### Test apps that deploy a real Cloudflare Worker
120+
121+
Apps like `cloudflare-workers-send-to-sentry` deploy the built app as a real Worker and check what arrives in Sentry:
122+
123+
- Use `createWorkerGlobalSetup` and `workerGlobalTeardown` from `@sentry-internal/test-utils/cloudflare` as the
124+
Playwright global setup and teardown. Tests read the Worker URL from `E2E_TEST_WORKER_URL`, send requests with
125+
`fetchFromWorker`, and poll Sentry with the helpers from `@sentry-internal/test-utils/cli` (the app needs `sentry` as
126+
a dev dependency).
127+
- The Worker gets `E2E_TEST_DSN`, `E2E_TEST_WORKER_TOKEN` and the values of the `secrets` option as secrets.
128+
- CI keeps the Worker after the run, and anyone can reach its `workers.dev` URL. If the Worker can spend money (for
129+
example, it holds an LLM API key), it must reject requests without `Authorization: Bearer <E2E_TEST_WORKER_TOKEN>`.
130+
The token is new for every deploy, and `fetchFromWorker` sends it. See
131+
`cloudflare-flue-send-to-sentry/src/auth.ts`.
132+
- Add the `workerPrefix` to `.github/workflows/cleanup-e2e-workers.yml`, which deletes the Worker of a PR when the PR
133+
closes.
134+
- Set `"sentryTest": { "optional": true }` in `package.json`, because only the optional CI job has the Cloudflare
135+
secrets.
136+
119137
## Troubleshooting
120138

121139
### Common Issues
Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,6 @@
1-
import { randomBytes } from 'node:crypto';
2-
import type { FullConfig } from '@playwright/test';
31
import { createWorkerGlobalSetup } from '@sentry-internal/test-utils/cloudflare';
42

5-
const deployWorker = createWorkerGlobalSetup({
3+
export default createWorkerGlobalSetup({
64
workerPrefix: 'e2e-flue-send-to-sentry',
7-
secrets: { OPENROUTER_API_KEY: 'E2E_OPENROUTER_API_KEY', E2E_TEST_WORKER_TOKEN: 'E2E_TEST_WORKER_TOKEN' },
5+
secrets: { OPENROUTER_API_KEY: 'E2E_OPENROUTER_API_KEY' },
86
});
9-
10-
/**
11-
* CI keeps the Worker after the run, and it holds an OpenRouter key. So its agent routes only accept
12-
* this token, which is new for every deploy and only known to the test run that deployed the Worker.
13-
*/
14-
export default async function globalSetup(config: FullConfig): Promise<void> {
15-
process.env.E2E_TEST_WORKER_TOKEN = randomBytes(32).toString('hex');
16-
await deployWorker(config);
17-
}

‎dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/auth.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { createMiddleware } from 'hono/factory';
22

33
/**
44
* CI keeps the Worker after the run, and it holds an OpenRouter key. So the agent routes only answer
5-
* the test run that deployed the Worker, which sends the token that global-setup.ts made for it.
5+
* the test run that deployed the Worker. `createWorkerGlobalSetup` makes the token for each deploy, and
6+
* `fetchFromWorker` sends it.
67
*/
78
export const requireTestToken = createMiddleware<{ Bindings: Env }>(async (c, next) => {
89
const token = c.env.E2E_TEST_WORKER_TOKEN;

‎dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tests/utils.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,16 @@ export function newInstanceId(prefix: string): string {
1515
*/
1616
export async function runAgentTurn(workerUrl: string, instanceId: string, message: string): Promise<string> {
1717
const url = `${workerUrl}/agents/hello/${instanceId}`;
18-
const authorization = `Bearer ${process.env.E2E_TEST_WORKER_TOKEN}`;
1918

2019
await fetchFromWorker(url, 202, {
2120
method: 'POST',
22-
headers: { 'Content-Type': 'application/json', Authorization: authorization },
21+
headers: { 'Content-Type': 'application/json' },
2322
body: JSON.stringify({ kind: 'user', body: message }),
2423
});
2524

2625
const deadline = Date.now() + 90_000;
2726
while (Date.now() < deadline) {
28-
const conversation = JSON.parse(await fetchFromWorker(url, 200, { headers: { Authorization: authorization } })) as {
27+
const conversation = JSON.parse(await fetchFromWorker(url, 200)) as {
2928
conversationId: string;
3029
settlements?: unknown[];
3130
};

‎dev-packages/test-utils/src/cloudflare.ts‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ export interface WorkerGlobalSetupOptions {
1212
*/
1313
workerPrefix: string;
1414
/**
15-
* The secrets of the Worker besides `E2E_TEST_DSN`, as a map from the binding name to the name of the
16-
* environment variable that holds the value.
15+
* The secrets of the Worker besides `E2E_TEST_DSN` and `E2E_TEST_WORKER_TOKEN`, as a map from the
16+
* binding name to the name of the environment variable that holds the value.
1717
*/
1818
secrets?: Record<string, string>;
1919
}
@@ -138,6 +138,11 @@ function getAppDir(config: FullConfig): string {
138138
/**
139139
* Returns a Playwright global setup that deploys the built test app as a real Worker. The tests read
140140
* its URL from `E2E_TEST_WORKER_URL`, and {@link workerGlobalTeardown} deletes it again.
141+
*
142+
* The Worker also gets the secret `E2E_TEST_WORKER_TOKEN`, a random value that is new for every deploy
143+
* and that {@link fetchFromWorker} sends as `Authorization: Bearer <token>`. CI keeps its Workers after
144+
* the run, so a Worker that can spend money (for example with an LLM API key) must reject requests
145+
* without this token.
141146
*/
142147
export function createWorkerGlobalSetup(options: WorkerGlobalSetupOptions): (config: FullConfig) => Promise<void> {
143148
return async config => {
@@ -153,8 +158,14 @@ export function createWorkerGlobalSetup(options: WorkerGlobalSetupOptions): (con
153158
throw new Error('CLOUDFLARE_ACCOUNT_ID must be set to deploy the test worker.');
154159
}
155160

161+
process.env.E2E_TEST_WORKER_TOKEN = randomBytes(32).toString('hex');
162+
156163
const secrets: Record<string, string> = {};
157-
for (const [binding, envName] of Object.entries({ E2E_TEST_DSN: 'E2E_TEST_DSN', ...options.secrets })) {
164+
for (const [binding, envName] of Object.entries({
165+
E2E_TEST_DSN: 'E2E_TEST_DSN',
166+
E2E_TEST_WORKER_TOKEN: 'E2E_TEST_WORKER_TOKEN',
167+
...options.secrets,
168+
})) {
158169
const value = process.env[envName];
159170
if (!value) {
160171
throw new Error(`${envName} must be set to deploy the test worker.`);
@@ -218,14 +229,21 @@ export function workerGlobalTeardown(config: FullConfig): void {
218229
* Workers Logs had no invocation for it. `status` is the status the Worker answers with. A Worker
219230
* that threw answers with status 500 and Cloudflare error code 1101, which sets it apart from a 500
220231
* that did not come from the Worker.
232+
*
233+
* The request carries the `E2E_TEST_WORKER_TOKEN` of {@link createWorkerGlobalSetup}, unless `init`
234+
* sets its own `Authorization` header.
221235
*/
222236
export async function fetchFromWorker(url: string, status: number, init?: RequestInit): Promise<string> {
223237
const deadline = Date.now() + 60_000;
224238
let lastAnswer = 'no answer';
239+
const headers = new Headers(init?.headers);
240+
if (process.env.E2E_TEST_WORKER_TOKEN && !headers.has('Authorization')) {
241+
headers.set('Authorization', `Bearer ${process.env.E2E_TEST_WORKER_TOKEN}`);
242+
}
225243

226244
while (Date.now() < deadline) {
227245
try {
228-
const response = await fetch(url, init);
246+
const response = await fetch(url, { ...init, headers });
229247
const body = await response.text();
230248
// Cloudflare sends its error page as HTML to some clients (Node's fetch among them) and as
231249
// `error code: <code>` plain text to others, so the code is read from either format.

0 commit comments

Comments
 (0)