Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 65 additions & 36 deletions packages/alchemy/test/Cloudflare/KV/Binding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,20 +241,36 @@ const exercise = (label: string, base: string) =>
yield* expectMissing(base, k("v"));
});

/** Write through `writeBase`, read it back (and observe the delete) through `readBase`. */
const crossWorker = (label: string, writeBase: string, readBase: string) =>
Effect.gen(function* () {
const key = `${label}-key`;
expect((yield* put(writeBase, key, `${label}-value`)).status).toBe(200);
expect(yield* expectValue(readBase, key, `${label}-value`)).toBe(
`${label}-value`,
);
yield* del(writeBase, key);
yield* expectMissing(readBase, key);
});

const url = (u: unknown) => {
expect(u).toBeTypeOf("string");
return u as string;
};

/**
* Deploys six Workers that all bind one shared KV namespace — read /
* write / read-write, each over the native Worker binding
* (`*NamespaceBinding`) and over a scoped HTTP API token
* (`*NamespaceHttp`) — then drives them over `fetch`:
* Deploys three Workers that all bind one shared KV namespace — read /
* write / read-write — over the native Worker binding
* (`*NamespaceBinding`), then drives them over `fetch`:
*
* - the read-write worker exercises the entire client surface by itself
* (every read + write method), once per transport;
* (every read + write method);
* - the split Read/Write workers prove the separate bindings agree on
* the namespace: write through Write, read back through Read, delete
* through Write, observe gone through Read.
*/
test.provider.skipIf(!!process.env.FAST)(
"KV read/write/read-write bindings over binding + http",
"KV read/write/read-write bindings over the native binding",
(stack) =>
Effect.gen(function* () {
yield* stack.destroy();
Expand All @@ -264,48 +280,61 @@ test.provider.skipIf(!!process.env.FAST)(
const readBinding = yield* ReadBindingWorker;
const writeBinding = yield* WriteBindingWorker;
const readWriteBinding = yield* ReadWriteBindingWorker;
const readHttp = yield* ReadHttpWorker;
const writeHttp = yield* WriteHttpWorker;
const readWriteHttp = yield* ReadWriteHttpWorker;
return {
readBinding: readBinding.url,
writeBinding: writeBinding.url,
readWriteBinding: readWriteBinding.url,
readHttp: readHttp.url,
writeHttp: writeHttp.url,
readWriteHttp: readWriteHttp.url,
};
}),
);

const url = (u: unknown) => {
expect(u).toBeTypeOf("string");
return u as string;
};

// ── Full client surface through a single read-write worker ──
yield* exercise("rw-bind", url(out.readWriteBinding));
yield* exercise("rw-http", url(out.readWriteHttp));
yield* crossWorker("bind", url(out.writeBinding), url(out.readBinding));

yield* stack.destroy();
}).pipe(logLevel),
{ timeout: 300_000 },
);

/**
* The same matrix over the `*NamespaceHttp` clients.
*
* Gated: the `*NamespaceHttp` layers mint a scoped `AccountApiToken`, and
* Cloudflare OAuth credentials have no token-creation scope at all — the
* deploy fails at token creation with:
*
* Unauthorized: Unauthorized to access requested resource
* at AccountApiToken.ts (provider.create)
*
* Deployed separately from the native-binding test above so that minting
* the token cannot take down binding coverage that needs no token.
*
* Set `CLOUDFLARE_TEST_API_TOKENS=1` with an API-token credential that is
* permitted to create account tokens. Matches the gate on the
* `UserApiToken` lifecycle tests (`CLOUDFLARE_TEST_USER_TOKENS`).
*/
test.provider.skipIf(
!!process.env.FAST || !process.env.CLOUDFLARE_TEST_API_TOKENS,
)(
"KV read/write/read-write bindings over a scoped HTTP token",
(stack) =>
Effect.gen(function* () {
yield* stack.destroy();

// ── Split Read/Write bindings agree on the shared namespace ──
const crossWorker = (
label: string,
writeBase: string,
readBase: string,
) =>
const out = yield* stack.deploy(
Effect.gen(function* () {
const key = `${label}-key`;
expect((yield* put(writeBase, key, `${label}-value`)).status).toBe(
200,
);
expect(yield* expectValue(readBase, key, `${label}-value`)).toBe(
`${label}-value`,
);
yield* del(writeBase, key);
yield* expectMissing(readBase, key);
});
const readHttp = yield* ReadHttpWorker;
const writeHttp = yield* WriteHttpWorker;
const readWriteHttp = yield* ReadWriteHttpWorker;
return {
readHttp: readHttp.url,
writeHttp: writeHttp.url,
readWriteHttp: readWriteHttp.url,
};
}),
);

yield* crossWorker("bind", url(out.writeBinding), url(out.readBinding));
yield* exercise("rw-http", url(out.readWriteHttp));
yield* crossWorker("http", url(out.writeHttp), url(out.readHttp));

yield* stack.destroy();
Expand Down
89 changes: 56 additions & 33 deletions packages/alchemy/test/Cloudflare/Queue/Binding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,54 +52,77 @@ const post = (base: string, path: string, body?: string) => {
};

/**
* Cloudflare Queue is producer-only at the binding layer, so there is
* no Read/ReadWrite split — only a Write producer. This deploys two
* Workers that both bind one shared queue (native Worker binding and
* scoped HTTP API token), then drives every {@link WriteQueueClient}
* method over `fetch` — `send` and `sendBatch`, each in its JSON and
* `text` content-type form — and asserts the producer accepts the
* messages (202), proving the binding/token are wired and reach the
* real queue.
* Drive every {@link WriteQueueClient} method over `fetch` — `send` and
* `sendBatch`, each in its JSON and `text` content-type form — and assert
* the producer accepts the messages (202), proving the binding/token are
* wired and reach the real queue.
*
* Cloudflare Queue is producer-only at the binding layer, so there is no
* Read/ReadWrite split — only a Write producer.
*
* The two implementations deploy separately on purpose. The HTTP producer
* mints a scoped `AccountApiToken`, which the native binding does not need;
* sharing one deploy meant a credential that cannot mint tokens lost the
* native-binding coverage too, rather than just the half it actually gates.
*/
const exercise = (base: string, label: string) =>
Effect.gen(function* () {
expect((yield* post(base, "/send", `${label}-json`)).status).toBe(202);
expect((yield* post(base, "/send-text", `${label}-text`)).status).toBe(202);
expect((yield* post(base, "/sendBatch")).status).toBe(202);
expect((yield* post(base, "/sendBatch-text")).status).toBe(202);
});

const url = (u: unknown) => {
expect(u).toBeTypeOf("string");
return u as string;
};

test.provider(
"Queue write producer over binding + http",
"Queue write producer over the native binding",
(stack) =>
Effect.gen(function* () {
yield* stack.destroy();

const out = yield* stack.deploy(
Effect.gen(function* () {
const writeBinding = yield* WriteBindingWorker;
const writeHttp = yield* WriteHttpWorker;
return {
writeBinding: writeBinding.url,
writeHttp: writeHttp.url,
};
return { writeBinding: writeBinding.url };
}),
);

const url = (u: unknown) => {
expect(u).toBeTypeOf("string");
return u as string;
};
yield* exercise(url(out.writeBinding), "binding");

// Drive the full producer surface (send + sendBatch, json + text)
// against one base url.
const exercise = (base: string, label: string) =>
yield* stack.destroy();
}).pipe(logLevel),
{ timeout: 240_000 },
);

/**
* Gated: the `WriteQueueHttp` layer mints a scoped `AccountApiToken`, and
* Cloudflare OAuth credentials have no token-creation scope at all — the
* deploy fails at token creation with:
*
* Unauthorized: Unauthorized to access requested resource
* at AccountApiToken.ts (provider.create)
*
* Set `CLOUDFLARE_TEST_API_TOKENS=1` with an API-token credential that is
* permitted to create account tokens. Matches the gate on the
* `UserApiToken` lifecycle tests (`CLOUDFLARE_TEST_USER_TOKENS`).
*/
test.provider.skipIf(!process.env.CLOUDFLARE_TEST_API_TOKENS)(
"Queue write producer over a scoped HTTP token",
(stack) =>
Effect.gen(function* () {
yield* stack.destroy();

const out = yield* stack.deploy(
Effect.gen(function* () {
expect((yield* post(base, "/send", `${label}-json`)).status).toBe(
202,
);
expect(
(yield* post(base, "/send-text", `${label}-text`)).status,
).toBe(202);
expect((yield* post(base, "/sendBatch")).status).toBe(202);
expect((yield* post(base, "/sendBatch-text")).status).toBe(202);
});
const writeHttp = yield* WriteHttpWorker;
return { writeHttp: writeHttp.url };
}),
);

// ── Native binding producer ──
yield* exercise(url(out.writeBinding), "binding");
// ── HTTP token producer ──
yield* exercise(url(out.writeHttp), "http");

yield* stack.destroy();
Expand Down
Loading
Loading