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

276 lines
7.3 KiB
TypeScript

import { ulid } from "ulidx";
import { it, expect, beforeEach, afterEach } from "vitest";
import { handleContentCreate, handleContentUpdate } from "../../../src/api/handlers/content.js";
import { SchemaRegistry } from "../../../src/schema/registry.js";
import {
describeEachDialect,
setupForDialect,
teardownForDialect,
type DialectTestContext,
} from "../../utils/test-db.js";
describeEachDialect("save-side media-field MIME validation", (dialect) => {
let ctx: DialectTestContext;
let pdfMediaId: string;
let zipMediaId: string;
beforeEach(async () => {
ctx = await setupForDialect(dialect);
// Create a posts collection with title and attachment fields
const registry = new SchemaRegistry(ctx.db);
await registry.createCollection({
slug: "posts",
label: "Posts",
labelSingular: "Post",
});
await registry.createField("posts", {
slug: "title",
label: "Title",
type: "string",
});
// Look up the collection id
const collection = await ctx.db
.selectFrom("_emdash_collections")
.select("id")
.where("slug", "=", "posts")
.executeTakeFirstOrThrow();
// Add a `file` field to posts that allows only PDFs
await ctx.db
.insertInto("_emdash_fields")
.values({
id: ulid(),
collection_id: collection.id,
slug: "attachment",
label: "Attachment",
type: "file",
column_type: "TEXT",
required: 0,
unique: 0,
default_value: null,
validation: JSON.stringify({ allowedMimeTypes: ["application/pdf"] }),
widget: "file",
options: null,
sort_order: 10,
})
.execute();
// Add the column to ec_posts
await ctx.db.schema.alterTable("ec_posts").addColumn("attachment", "text").execute();
// Seed two media items
pdfMediaId = ulid();
zipMediaId = ulid();
await ctx.db
.insertInto("media")
.values([
{
id: pdfMediaId,
filename: "doc.pdf",
mime_type: "application/pdf",
size: 100,
width: null,
height: null,
alt: null,
caption: null,
storage_key: "doc.pdf",
content_hash: null,
blurhash: null,
dominant_color: null,
status: "ready",
author_id: null,
},
{
id: zipMediaId,
filename: "x.zip",
mime_type: "application/zip",
size: 100,
width: null,
height: null,
alt: null,
caption: null,
storage_key: "x.zip",
content_hash: null,
blurhash: null,
dominant_color: null,
status: "ready",
author_id: null,
},
])
.execute();
});
afterEach(async () => {
await teardownForDialect(ctx);
});
it("accepts a PDF in a PDF-only field", async () => {
const result = await handleContentCreate(ctx.db, "posts", {
slug: "p1",
data: {
title: "p1",
attachment: { id: pdfMediaId, provider: "local", filename: "doc.pdf" },
},
});
expect(result.success).toBe(true);
});
it("rejects a zip in a PDF-only field on create", async () => {
const result = await handleContentCreate(ctx.db, "posts", {
slug: "p2",
data: {
title: "p2",
attachment: { id: zipMediaId, provider: "local", filename: "x.zip" },
},
});
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error.code).toBe("INVALID_MIME_FOR_FIELD");
});
it("rejects a zip in a PDF-only field on update", async () => {
const created = await handleContentCreate(ctx.db, "posts", {
slug: "p3",
data: { title: "p3" },
});
if (!created.success) throw new Error("seed failed");
const result = await handleContentUpdate(ctx.db, "posts", created.data.item.id, {
data: {
attachment: { id: zipMediaId, provider: "local", filename: "x.zip" },
},
});
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error.code).toBe("INVALID_MIME_FOR_FIELD");
});
it("rejects external-provider ref when mimeType is present and does not match allowlist", async () => {
const result = await handleContentCreate(ctx.db, "posts", {
slug: "p4",
data: {
title: "p4",
attachment: {
id: "ext-1",
provider: "s3",
filename: "remote.zip",
mimeType: "application/zip",
src: "https://example.com/remote.zip",
},
},
});
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error.code).toBe("INVALID_MIME_FOR_FIELD");
});
it("accepts external-provider ref when mimeType matches the allowlist", async () => {
const result = await handleContentCreate(ctx.db, "posts", {
slug: "p4b",
data: {
title: "p4b",
attachment: {
id: "ext-2",
provider: "s3",
filename: "remote.pdf",
mimeType: "application/pdf",
src: "https://example.com/remote.pdf",
},
},
});
expect(result.success).toBe(true);
});
it("rejects external-provider ref with no mimeType when field is constrained", async () => {
const result = await handleContentCreate(ctx.db, "posts", {
slug: "p4c",
data: {
title: "p4c",
attachment: {
id: "ext-3",
provider: "s3",
filename: "remote-unknown",
src: "https://example.com/remote-unknown",
},
},
});
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error.code).toBe("INVALID_MIME_FOR_FIELD");
});
it("rejects local-provider ref with non-string id when field is constrained", async () => {
const result = await handleContentCreate(ctx.db, "posts", {
slug: "p4d",
data: {
title: "p4d",
attachment: { id: 123, provider: "local", filename: "doc.pdf" },
},
});
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error.code).toBe("INVALID_MIME_FOR_FIELD");
});
it("file/image field without allowedMimeTypes is not validated", async () => {
// Insert a second file field with no MIME restrictions (backwards-compat assertion)
const collection = await ctx.db
.selectFrom("_emdash_collections")
.select("id")
.where("slug", "=", "posts")
.executeTakeFirstOrThrow();
await ctx.db
.insertInto("_emdash_fields")
.values({
id: ulid(),
collection_id: collection.id,
slug: "unconstrained",
label: "Unconstrained",
type: "file",
column_type: "TEXT",
required: 0,
unique: 0,
default_value: null,
validation: null,
widget: "file",
options: null,
sort_order: 20,
})
.execute();
await ctx.db.schema.alterTable("ec_posts").addColumn("unconstrained", "text").execute();
// Attaching a zip to the unconstrained field alongside a valid PDF in the
// constrained field — the save should succeed because unconstrained has no
// allowedMimeTypes.
const result = await handleContentCreate(ctx.db, "posts", {
slug: "p5",
data: {
title: "p5",
attachment: { id: pdfMediaId, provider: "local", filename: "doc.pdf" },
unconstrained: { id: zipMediaId, provider: "local", filename: "x.zip" },
},
});
expect(result.success).toBe(true);
});
it("local media ID not found in DB returns INVALID_MIME_FOR_FIELD", async () => {
// Reference a made-up ULID that doesn't exist in the media table
const missingId = ulid();
const result = await handleContentCreate(ctx.db, "posts", {
slug: "p6",
data: {
title: "p6",
attachment: { id: missingId, provider: "local", filename: "ghost.pdf" },
},
});
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error.code).toBe("INVALID_MIME_FOR_FIELD");
});
});