Files
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

116 lines
3.4 KiB
TypeScript

/**
* Trusted plugin API routes must receive a writable `ctx.media`.
*
* Regression: `EmDashRuntime.handlePluginApiRoute` built a `PluginRouteRegistry`
* without threading `storage`, so a `media:write` plugin invoked via
* `/_emdash/api/plugin/{id}/{route}` got a read-only (or undefined) `ctx.media`
* and `ctx.media.upload()` was unusable — the exact trigger in the bug report.
*/
import { randomUUID } from "node:crypto";
import Database from "better-sqlite3";
import { SqliteDialect } from "kysely";
import { describe, expect, it } from "vitest";
import { EmDashRuntime } from "../../../src/emdash-runtime.js";
import type { RuntimeDependencies } from "../../../src/emdash-runtime.js";
import { definePlugin } from "../../../src/plugins/define-plugin.js";
import type { Storage } from "../../../src/storage/types.js";
/** Minimal in-memory Storage backend that records uploaded keys. */
function createFakeStorage() {
const uploads = new Map<string, Uint8Array>();
const storage: Storage = {
async upload(options) {
const body =
options.body instanceof Uint8Array
? options.body
: new Uint8Array(options.body as ArrayBuffer);
uploads.set(options.key, body);
return { key: options.key, size: body.byteLength };
},
async download() {
throw new Error("not implemented");
},
async delete(key) {
uploads.delete(key);
},
async exists(key) {
return uploads.has(key);
},
async list() {
return { items: [] };
},
async getSignedUploadUrl(options) {
return {
url: `https://signed.example.com/${options.key}`,
method: "PUT",
headers: {},
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
};
},
getPublicUrl(key) {
return `/media/${key}`;
},
};
return { storage, uploads };
}
function createDeps(storage: Storage): RuntimeDependencies {
const entrypoint = `test-plugin-media-route-${randomUUID()}`;
return {
config: {
database: { entrypoint, config: {}, type: "sqlite" },
storage: { entrypoint, config: {} },
},
plugins: [
definePlugin({
id: "media-uploader",
version: "1.0.0",
capabilities: ["media:write"],
routes: {
upload: {
handler: async (ctx) => {
const bytes = new Uint8Array([1, 2, 3, 4]).buffer;
return ctx.media!.upload!("from-route.png", "image/png", bytes);
},
},
},
}),
],
createDialect: () => new SqliteDialect({ database: new Database(":memory:") }),
createStorage: () => storage,
sandboxEnabled: false,
sandboxedPluginEntries: [],
createSandboxRunner: null,
};
}
describe("EmDashRuntime.handlePluginApiRoute — media:write", () => {
it("provides a writable ctx.media so a trusted plugin route can upload", async () => {
const { storage, uploads } = createFakeStorage();
const runtime = await EmDashRuntime.create(createDeps(storage));
try {
const result = await runtime.handlePluginApiRoute(
"media-uploader",
"POST",
"/upload",
new Request("http://test.local/_emdash/api/plugin/media-uploader/upload", {
method: "POST",
}),
);
expect(result.success).toBe(true);
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowing the route result for the assertion
const data = result.data as { mediaId: string; storageKey: string };
expect(data.mediaId).toBeTruthy();
expect(data.storageKey).toMatch(/\.png$/);
expect(uploads.has(data.storageKey)).toBe(true);
} finally {
await runtime.stopCron();
}
});
});