From fb8cbd1972e0c223c75a02fc1977162f79346680 Mon Sep 17 00:00:00 2001 From: Samuel Macleod Date: Thu, 26 Mar 2026 18:39:35 +0000 Subject: [PATCH 1/4] Add adminSecretsStore() to cloudflare:test for seeding secrets in tests Secrets store bindings only expose .get(), so there was no way to seed values in vitest-pool-workers tests. This exposes Miniflare's admin API through a new cloudflare:test import, giving tests create/update/delete access. Fixes #12778 --- ...test-pool-workers-secrets-store-persist.md | 17 ++++++++ .../worker/lib/cloudflare/test-internal.ts | 1 + .../src/worker/lib/cloudflare/test.ts | 1 + .../src/worker/secrets-store.ts | 39 ++++++++++++++++++ .../vitest-pool-workers/test/bindings.test.ts | 41 +++++++++++++++++++ .../types/cloudflare-test.d.ts | 38 +++++++++++++++++ 6 files changed, 137 insertions(+) create mode 100644 .changeset/vitest-pool-workers-secrets-store-persist.md create mode 100644 packages/vitest-pool-workers/src/worker/secrets-store.ts diff --git a/.changeset/vitest-pool-workers-secrets-store-persist.md b/.changeset/vitest-pool-workers-secrets-store-persist.md new file mode 100644 index 0000000000..26d44bb4fe --- /dev/null +++ b/.changeset/vitest-pool-workers-secrets-store-persist.md @@ -0,0 +1,17 @@ +--- +"@cloudflare/vitest-pool-workers": patch +--- + +Add `adminSecretsStore()` to `cloudflare:test` for seeding secrets in tests + +Secrets store bindings only expose a read-only `.get()` method, so there was previously no way to seed secret values from within a test. The new `adminSecretsStore()` helper returns Miniflare's admin API for a secrets store binding, giving tests full control over create, update, and delete operations. + +```ts +import { adminSecretsStore } from "cloudflare:test"; +import { env } from "cloudflare:workers"; + +const admin = adminSecretsStore(env.MY_SECRET); +await admin.create("test-value"); + +const value = await env.MY_SECRET.get(); // "test-value" +``` diff --git a/packages/vitest-pool-workers/src/worker/lib/cloudflare/test-internal.ts b/packages/vitest-pool-workers/src/worker/lib/cloudflare/test-internal.ts index 595f283a80..89e406fe62 100644 --- a/packages/vitest-pool-workers/src/worker/lib/cloudflare/test-internal.ts +++ b/packages/vitest-pool-workers/src/worker/lib/cloudflare/test-internal.ts @@ -7,5 +7,6 @@ export * from "../../durable-objects"; export * from "../../entrypoints"; export * from "../../env"; export * from "../../events"; +export * from "../../secrets-store"; export * from "../../wait-until"; export * from "../../workflows"; diff --git a/packages/vitest-pool-workers/src/worker/lib/cloudflare/test.ts b/packages/vitest-pool-workers/src/worker/lib/cloudflare/test.ts index a89acf2a8e..6104caecf6 100644 --- a/packages/vitest-pool-workers/src/worker/lib/cloudflare/test.ts +++ b/packages/vitest-pool-workers/src/worker/lib/cloudflare/test.ts @@ -16,6 +16,7 @@ export { createMessageBatch, getQueueResult, applyD1Migrations, + adminSecretsStore, createPagesEventContext, introspectWorkflowInstance, introspectWorkflow, diff --git a/packages/vitest-pool-workers/src/worker/secrets-store.ts b/packages/vitest-pool-workers/src/worker/secrets-store.ts new file mode 100644 index 0000000000..48d9f0341d --- /dev/null +++ b/packages/vitest-pool-workers/src/worker/secrets-store.ts @@ -0,0 +1,39 @@ +// The admin API key used by Miniflare's emulated secrets store binding. +// This must match the value in miniflare/src/workers/secrets-store/constants.ts. +const ADMIN_API = "SecretsStoreSecret::admin_api"; + +/** + * Returns the admin API for a secrets store binding, allowing tests to + * create, update, and delete secrets that would otherwise be read-only. + * + * ```ts + * import { adminSecretsStore } from "cloudflare:test"; + * + * const admin = adminSecretsStore(env.MY_SECRET); + * await admin.create("my-secret-value"); + * ``` + */ +export function adminSecretsStore(binding: unknown): SecretsStoreSecretAdmin { + if ( + typeof binding !== "object" || + binding === null || + typeof (binding as Record)[ADMIN_API] !== "function" + ) { + throw new TypeError( + "Failed to execute 'adminSecretsStore': parameter 1 is not a secrets store binding." + ); + } + + return (binding as Record unknown>)[ + ADMIN_API + ]() as SecretsStoreSecretAdmin; +} + +interface SecretsStoreSecretAdmin { + create(value: string): Promise; + update(value: string, id: string): Promise; + duplicate(id: string, newName: string): Promise; + delete(id: string): Promise; + list(): Promise<{ name: string; metadata?: { uuid: string } }[]>; + get(id: string): Promise; +} diff --git a/packages/vitest-pool-workers/test/bindings.test.ts b/packages/vitest-pool-workers/test/bindings.test.ts index d9f25ecba4..2394f3ee67 100644 --- a/packages/vitest-pool-workers/test/bindings.test.ts +++ b/packages/vitest-pool-workers/test/bindings.test.ts @@ -51,3 +51,44 @@ test("hello_world support", async ({ expect, seed, vitestRun }) => { await expect(result.exitCode).resolves.toBe(0); }); + +test("adminSecretsStore seeds and reads secrets", async ({ + expect, + seed, + vitestRun, +}) => { + await seed({ + "vitest.config.mts": vitestConfig({ + wrangler: { configPath: "./wrangler.jsonc" }, + }), + "wrangler.jsonc": dedent` + { + "name": "test-worker", + "compatibility_date": "2025-12-02", + "compatibility_flags": ["nodejs_compat"], + "secrets_store_secrets": [ + { + "binding": "MY_SECRET", + "secret_name": "my-secret", + "store_id": "aaaabbbbccccdddd0000000000000000" + } + ] + } + `, + "index.test.ts": dedent` + import { adminSecretsStore } from "cloudflare:test"; + import { env } from "cloudflare:workers"; + import { it } from "vitest"; + + it("seeds and retrieves a secret", async ({ expect }) => { + const admin = adminSecretsStore(env.MY_SECRET); + await admin.create("test-value"); + const value = await env.MY_SECRET.get(); + expect(value).toBe("test-value"); + }); + `, + }); + + const result = await vitestRun(); + await expect(result.exitCode).resolves.toBe(0); +}); diff --git a/packages/vitest-pool-workers/types/cloudflare-test.d.ts b/packages/vitest-pool-workers/types/cloudflare-test.d.ts index 002f540029..8e63b1e52f 100644 --- a/packages/vitest-pool-workers/types/cloudflare-test.d.ts +++ b/packages/vitest-pool-workers/types/cloudflare-test.d.ts @@ -102,6 +102,44 @@ declare module "cloudflare:test" { migrationsTableName?: string ): Promise; + /** + * Admin API for a secrets store binding. Returned by `adminSecretsStore()`. + */ + export interface SecretsStoreSecretAdmin { + /** Create a new secret with the given value. Returns the secret's ID. */ + create(value: string): Promise; + /** Update an existing secret (identified by ID) with a new value. Returns the secret's ID. */ + update(value: string, id: string): Promise; + /** Duplicate a secret (identified by ID) under a new name. Returns the new secret's ID. */ + duplicate(id: string, newName: string): Promise; + /** Delete a secret by ID. */ + delete(id: string): Promise; + /** List all secrets in the store. */ + list(): Promise<{ name: string; metadata?: { uuid: string } }[]>; + /** Get a secret's name by ID. */ + get(id: string): Promise; + } + + /** + * Returns the admin API for a secrets store binding, allowing tests to + * create, update, and delete secrets that would otherwise be read-only + * via `binding.get()`. + * + * @example + * ```ts + * import { adminSecretsStore } from "cloudflare:test"; + * import { env } from "cloudflare:workers"; + * + * const admin = adminSecretsStore(env.MY_SECRET); + * await admin.create("my-secret-value"); + * + * // Now env.MY_SECRET.get() will return "my-secret-value" + * ``` + */ + export function adminSecretsStore(binding: { + get(): Promise; + }): SecretsStoreSecretAdmin; + /** * Creates an introspector for a specific Workflow instance, used to * modify its behavior and await outcomes. From fffb20cc1f7c2b171c2f82ef7f16f9e0ec6e92fd Mon Sep 17 00:00:00 2001 From: Samuel Macleod Date: Fri, 27 Mar 2026 01:05:36 +0000 Subject: [PATCH 2/4] Address review: import ADMIN_API and type from miniflare, expand test coverage - Export SECRETS_STORE_ADMIN_API constant and SecretsStoreSecretAdmin interface from miniflare instead of duplicating them - Import ADMIN_API from miniflare source (same pattern as devalue import) - Import SecretsStoreSecretAdmin type from miniflare - Exercise create, update, list, and delete in the test --- packages/miniflare/src/index.ts | 22 ++++++++------- .../src/worker/secrets-store.ts | 14 ++-------- .../vitest-pool-workers/test/bindings.test.ts | 27 ++++++++++++++++--- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index 6eca63cd3c..9c0c9093ef 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -2711,16 +2711,7 @@ export class Miniflare { getSecretsStoreSecretAPI( bindingName: string, workerName?: string - ): Promise< - () => { - create: (value: string) => Promise; - update: (value: string, id: string) => Promise; - duplicate: (id: string, newName: string) => Promise; - delete: (id: string) => Promise; - list: () => Promise[]>; - get: (id: string) => Promise; - } - > { + ): Promise<() => SecretsStoreSecretAdmin> { return this.#getProxy( SECRET_STORE_PLUGIN_NAME, bindingName, @@ -2827,6 +2818,17 @@ export class Miniflare { export type { WorkerdStructuredLog } from "./plugins/core"; +export { ADMIN_API as SECRETS_STORE_ADMIN_API } from "./workers/secrets-store/constants"; + +export interface SecretsStoreSecretAdmin { + create(value: string): Promise; + update(value: string, id: string): Promise; + duplicate(id: string, newName: string): Promise; + delete(id: string): Promise; + list(): Promise[]>; + get(id: string): Promise; +} + export * from "./http"; export * from "./plugins"; export * from "./runtime"; diff --git a/packages/vitest-pool-workers/src/worker/secrets-store.ts b/packages/vitest-pool-workers/src/worker/secrets-store.ts index 48d9f0341d..dbd882bba4 100644 --- a/packages/vitest-pool-workers/src/worker/secrets-store.ts +++ b/packages/vitest-pool-workers/src/worker/secrets-store.ts @@ -1,6 +1,5 @@ -// The admin API key used by Miniflare's emulated secrets store binding. -// This must match the value in miniflare/src/workers/secrets-store/constants.ts. -const ADMIN_API = "SecretsStoreSecret::admin_api"; +import { ADMIN_API } from "../../../miniflare/src/workers/secrets-store/constants"; +import type { SecretsStoreSecretAdmin } from "miniflare"; /** * Returns the admin API for a secrets store binding, allowing tests to @@ -28,12 +27,3 @@ export function adminSecretsStore(binding: unknown): SecretsStoreSecretAdmin { ADMIN_API ]() as SecretsStoreSecretAdmin; } - -interface SecretsStoreSecretAdmin { - create(value: string): Promise; - update(value: string, id: string): Promise; - duplicate(id: string, newName: string): Promise; - delete(id: string): Promise; - list(): Promise<{ name: string; metadata?: { uuid: string } }[]>; - get(id: string): Promise; -} diff --git a/packages/vitest-pool-workers/test/bindings.test.ts b/packages/vitest-pool-workers/test/bindings.test.ts index 2394f3ee67..7fc5417a9a 100644 --- a/packages/vitest-pool-workers/test/bindings.test.ts +++ b/packages/vitest-pool-workers/test/bindings.test.ts @@ -80,11 +80,30 @@ test("adminSecretsStore seeds and reads secrets", async ({ import { env } from "cloudflare:workers"; import { it } from "vitest"; - it("seeds and retrieves a secret", async ({ expect }) => { + it("create, update, list, and delete a secret", async ({ expect }) => { const admin = adminSecretsStore(env.MY_SECRET); - await admin.create("test-value"); - const value = await env.MY_SECRET.get(); - expect(value).toBe("test-value"); + + // create + const id = await admin.create("initial-value"); + expect(typeof id).toBe("string"); + expect(await env.MY_SECRET.get()).toBe("initial-value"); + + // update + await admin.update("updated-value", id); + expect(await env.MY_SECRET.get()).toBe("updated-value"); + + // list + const secrets = await admin.list(); + expect(secrets.length).toBeGreaterThan(0); + + // delete + await admin.delete(id); + try { + await env.MY_SECRET.get(); + expect.unreachable("expected get() to throw after delete"); + } catch (e) { + expect(String(e)).toContain("not found"); + } }); `, }); From aa86b63d7028d9264760a45167e767c23d4d1c49 Mon Sep 17 00:00:00 2001 From: Samuel Macleod Date: Fri, 27 Mar 2026 01:17:26 +0000 Subject: [PATCH 3/4] Copy ADMIN_API constant with comment instead of cross-package source import --- packages/vitest-pool-workers/src/worker/secrets-store.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vitest-pool-workers/src/worker/secrets-store.ts b/packages/vitest-pool-workers/src/worker/secrets-store.ts index dbd882bba4..9a3089153d 100644 --- a/packages/vitest-pool-workers/src/worker/secrets-store.ts +++ b/packages/vitest-pool-workers/src/worker/secrets-store.ts @@ -1,6 +1,8 @@ -import { ADMIN_API } from "../../../miniflare/src/workers/secrets-store/constants"; import type { SecretsStoreSecretAdmin } from "miniflare"; +// Must match ADMIN_API in miniflare/src/workers/secrets-store/constants.ts +const ADMIN_API = "SecretsStoreSecret::admin_api"; + /** * Returns the admin API for a secrets store binding, allowing tests to * create, update, and delete secrets that would otherwise be read-only. From 0782d74bdcbc51019159554051286759b54e2c69 Mon Sep 17 00:00:00 2001 From: Samuel Macleod Date: Fri, 27 Mar 2026 01:19:04 +0000 Subject: [PATCH 4/4] Remove unused SECRETS_STORE_ADMIN_API export from miniflare --- packages/miniflare/src/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index 9c0c9093ef..977f48440d 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -2818,8 +2818,6 @@ export class Miniflare { export type { WorkerdStructuredLog } from "./plugins/core"; -export { ADMIN_API as SECRETS_STORE_ADMIN_API } from "./workers/secrets-store/constants"; - export interface SecretsStoreSecretAdmin { create(value: string): Promise; update(value: string, id: string): Promise;