53 lines
4.1 KiB
TypeScript
53 lines
4.1 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import { test } from "node:test"
|
|
import { downloadHumanArtifact, humanArtifactError, humanArtifactReceipt } from "./mcp-artifact"
|
|
|
|
const id = "fa58fa40-4bcb-4616-9316-cb69463b3150", path = `/api/mcp/artifacts/${id}`
|
|
const receipt = { artifactId: id, humanDownloadOnly: true, downloadPath: path }
|
|
|
|
test("artifact paths come exclusively from a UUID and a matching structured receipt", () => {
|
|
assert.deepEqual(humanArtifactReceipt(receipt), { artifactId: id, path })
|
|
for (const invalid of [null, path, { ...receipt, artifactId: "../secrets" }, { ...receipt, artifactId: id + "?token=secret" }, { ...receipt, humanDownloadOnly: false }, ...["https://unsafe.test/", "//unsafe.test/", "javascript:alert(1)", path + "?token=secret", path + "/../x"].map(downloadPath => ({ ...receipt, downloadPath }))]) assert.equal(humanArtifactReceipt(invalid), null)
|
|
})
|
|
|
|
test("binary download uses the portal JWT, no redirects, temporary Blob URL and safe errors", async () => {
|
|
const globals = ["window", "document", "localStorage", "setTimeout"] as const
|
|
const descriptors = globals.map(name => [name, Object.getOwnPropertyDescriptor(globalThis, name)] as const)
|
|
const originalFetch = globalThis.fetch, originalCreate = URL.createObjectURL, originalRevoke = URL.revokeObjectURL
|
|
let token: string | null = "header." + btoa(JSON.stringify({ sub: "human", apps: ["mm"], exp: 4102444800 })) + ".signature"
|
|
let clicks = 0, removed = 0, revoked = 0, requests = 0
|
|
const anchor = { href: "", download: "", click: () => { clicks++ }, remove: () => { removed++ } }
|
|
try {
|
|
Object.defineProperty(globalThis, "window", { configurable: true, value: { location: { hostname: "localhost" } } })
|
|
Object.defineProperty(globalThis, "localStorage", { configurable: true, value: { getItem: () => token, removeItem: () => { token = null } } })
|
|
Object.defineProperty(globalThis, "document", { configurable: true, value: { createElement: () => anchor, body: { appendChild: () => undefined } } })
|
|
Object.defineProperty(globalThis, "setTimeout", { configurable: true, value: (callback: () => void) => { callback(); return 0 } })
|
|
URL.createObjectURL = blob => { assert.ok(blob instanceof Blob); assert.equal(blob.size, 4); return "blob:temporary" }
|
|
URL.revokeObjectURL = url => { assert.equal(url, "blob:temporary"); revoked++ }
|
|
globalThis.fetch = async (url, init) => {
|
|
requests++
|
|
assert.ok(String(url).endsWith(path)); assert.equal(new Headers(init?.headers).get("Authorization"), `Bearer ${token}`)
|
|
assert.equal(init?.redirect, "error"); assert.equal(init?.cache, "no-store")
|
|
return new Response(new Uint8Array([0, 255, 128, 1]), { headers: { "Content-Disposition": 'attachment; filename="backup.rsc"' } })
|
|
}
|
|
await downloadHumanArtifact("http://localhost:8000", receipt)
|
|
assert.equal(anchor.href, "blob:temporary"); assert.equal(anchor.download, "backup.rsc")
|
|
assert.equal(clicks, 1); assert.equal(removed, 1); assert.equal(revoked, 1)
|
|
const cancelled = new AbortController(); cancelled.abort()
|
|
await assert.rejects(downloadHumanArtifact("http://localhost:8000", receipt, cancelled.signal)); assert.equal(clicks, 1)
|
|
for (const status of [403, 404, 409, 500]) {
|
|
globalThis.fetch = async () => new Response(JSON.stringify({ error: "NEVER_RENDER_SECRET" }), { status })
|
|
await assert.rejects(downloadHumanArtifact("http://localhost:8000", receipt), error => !humanArtifactError(error).includes("NEVER_RENDER_SECRET"))
|
|
}
|
|
token = "mmcp_bearer_not_a_portal_JWT"
|
|
globalThis.fetch = async () => { throw Error("must not request") }
|
|
await assert.rejects(downloadHumanArtifact("http://localhost:8000", receipt), error => humanArtifactError(error).includes("Войдите в портал"))
|
|
token = null
|
|
await assert.rejects(downloadHumanArtifact("http://localhost:8000", receipt))
|
|
assert.equal(requests, 2)
|
|
} finally {
|
|
globalThis.fetch = originalFetch; URL.createObjectURL = originalCreate; URL.revokeObjectURL = originalRevoke
|
|
for (const [name, descriptor] of descriptors) { if (descriptor) Object.defineProperty(globalThis, name, descriptor); else Reflect.deleteProperty(globalThis, name) }
|
|
}
|
|
})
|