Skip to content
Merged
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
17 changes: 17 additions & 0 deletions .changeset/vitest-pool-workers-secrets-store-persist.md
Original file line number Diff line number Diff line change
@@ -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"
```
20 changes: 10 additions & 10 deletions packages/miniflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2711,16 +2711,7 @@ export class Miniflare {
getSecretsStoreSecretAPI(
bindingName: string,
workerName?: string
): Promise<
() => {
create: (value: string) => Promise<string>;
update: (value: string, id: string) => Promise<string>;
duplicate: (id: string, newName: string) => Promise<string>;
delete: (id: string) => Promise<void>;
list: () => Promise<KVNamespaceListKey<{ uuid: string }, string>[]>;
get: (id: string) => Promise<string>;
}
> {
): Promise<() => SecretsStoreSecretAdmin> {
return this.#getProxy(
SECRET_STORE_PLUGIN_NAME,
bindingName,
Expand Down Expand Up @@ -2827,6 +2818,15 @@ export class Miniflare {

export type { WorkerdStructuredLog } from "./plugins/core";

export interface SecretsStoreSecretAdmin {
create(value: string): Promise<string>;
update(value: string, id: string): Promise<string>;
duplicate(id: string, newName: string): Promise<string>;
delete(id: string): Promise<void>;
list(): Promise<KVNamespaceListKey<{ uuid: string }, string>[]>;
get(id: string): Promise<string>;
}

export * from "./http";
export * from "./plugins";
export * from "./runtime";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export {
createMessageBatch,
getQueueResult,
applyD1Migrations,
adminSecretsStore,
createPagesEventContext,
introspectWorkflowInstance,
introspectWorkflow,
Expand Down
31 changes: 31 additions & 0 deletions packages/vitest-pool-workers/src/worker/secrets-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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.
*
* ```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<string, unknown>)[ADMIN_API] !== "function"
) {
throw new TypeError(
"Failed to execute 'adminSecretsStore': parameter 1 is not a secrets store binding."
);
}

return (binding as Record<string, (...args: unknown[]) => unknown>)[
ADMIN_API
]() as SecretsStoreSecretAdmin;
}
60 changes: 60 additions & 0 deletions packages/vitest-pool-workers/test/bindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,63 @@ 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("create, update, list, and delete a secret", async ({ expect }) => {
const admin = adminSecretsStore(env.MY_SECRET);

// 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");
}
});
`,
});

const result = await vitestRun();
await expect(result.exitCode).resolves.toBe(0);
});
38 changes: 38 additions & 0 deletions packages/vitest-pool-workers/types/cloudflare-test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,44 @@ declare module "cloudflare:test" {
migrationsTableName?: string
): Promise<void>;

/**
* 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<string>;
/** Update an existing secret (identified by ID) with a new value. Returns the secret's ID. */
update(value: string, id: string): Promise<string>;
/** Duplicate a secret (identified by ID) under a new name. Returns the new secret's ID. */
duplicate(id: string, newName: string): Promise<string>;
/** Delete a secret by ID. */
delete(id: string): Promise<void>;
/** List all secrets in the store. */
list(): Promise<{ name: string; metadata?: { uuid: string } }[]>;
/** Get a secret's name by ID. */
get(id: string): Promise<string>;
}

/**
* 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<string>;
}): SecretsStoreSecretAdmin;

/**
* Creates an introspector for a specific Workflow instance, used to
* modify its behavior and await outcomes.
Expand Down
Loading