364 lines
28 KiB
TypeScript
364 lines
28 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { createHmac } from "node:crypto";
|
|
import { createHttpServer } from "../src/app/http.ts";
|
|
import { loadConfig } from "../src/config.ts";
|
|
import { createTotpSecret, encryptSecret, issueAccessToken, verifyAccessToken } from "../src/shared/auth.ts";
|
|
import { createStore } from "../src/store.ts";
|
|
|
|
function config(role = "operator") {
|
|
return { host: "127.0.0.1", port: 0, accessTokenSecret: "test-secret-for-security-tests", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: false, userMfaRequired: false, adminRole: role };
|
|
}
|
|
|
|
test("ordinary-user verification enhancements default to disabled", () => {
|
|
const config = loadConfig({ NODE_ENV: "development", MIRAGENFLOW_ACCESS_TOKEN_SECRET: "default-test-secret" });
|
|
assert.equal(config.adminMfaRequired, false);
|
|
assert.equal(config.userMfaRequired, false);
|
|
assert.equal(config.userVerificationRequired, false);
|
|
assert.equal(config.captchaRequired, false);
|
|
assert.equal(config.adminAccounts?.[0]?.mfaRequired, false);
|
|
});
|
|
|
|
test("cookie security can be explicitly disabled for an HTTP reverse proxy", () => {
|
|
const productionEnv = {
|
|
NODE_ENV: "production",
|
|
MIRAGENFLOW_ACCESS_TOKEN_SECRET: "a-production-secret-that-is-long-enough",
|
|
MIRAGENFLOW_ADMIN_PASSWORD: "a-production-admin-password",
|
|
MIRAGENFLOW_ADMIN_MFA_SECRET: "JBSWY3DPEHPK3PXP",
|
|
MIRAGENFLOW_CHANNEL_ENCRYPTION_KEY: "a-production-channel-key",
|
|
MIRAGENFLOW_PERSISTENCE_ADAPTER: "postgres",
|
|
DATABASE_URL: "postgres://example",
|
|
MIRAGENFLOW_QUEUE_ADAPTER: "redis",
|
|
REDIS_URL: "redis://example",
|
|
};
|
|
const productionDefaults = loadConfig(productionEnv);
|
|
assert.equal(productionDefaults.cookieSecure, true);
|
|
assert.equal(loadConfig({ ...productionEnv, MIRAGENFLOW_COOKIE_SECURE: "false" }).cookieSecure, false);
|
|
assert.equal(loadConfig({ NODE_ENV: "development", MIRAGENFLOW_ACCESS_TOKEN_SECRET: "default-test-secret", MIRAGENFLOW_COOKIE_SECURE: "true" }).cookieSecure, true);
|
|
});
|
|
|
|
function totpCode(secret: string, now = Date.now()) {
|
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; let buffer = 0; let bits = 0; const bytes: number[] = [];
|
|
for (const character of secret) { const value = alphabet.indexOf(character); if (value < 0) continue; buffer = (buffer << 5) | value; bits += 5; if (bits >= 8) { bytes.push((buffer >>> (bits - 8)) & 255); bits -= 8; } }
|
|
const key = Buffer.from(bytes); const message = Buffer.alloc(8); message.writeBigInt64BE(BigInt(Math.floor(now / 30_000))); const digest = createHmac("sha1", key).update(message).digest(); const offset = digest[digest.length - 1] & 15; const value = ((digest[offset] & 127) << 24) | (digest[offset + 1] << 16) | (digest[offset + 2] << 8) | digest[offset + 3]; return String(value % 1_000_000).padStart(6, "0");
|
|
}
|
|
|
|
test("CAPTCHA challenge is hashed, bound, retry-limited, and one-time", async () => {
|
|
const store = createStore();
|
|
const server = createHttpServer(store, { ...config(), captchaRequired: true });
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
const address = server.address(); assert.ok(address && typeof address !== "string");
|
|
const base = `http://127.0.0.1:${address.port}`; const email = "captcha@example.com"; const deviceId = "captcha-device";
|
|
try {
|
|
const challengeResponse = await fetch(`${base}/api/v1/auth/challenge`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "register", target: email, deviceId }) });
|
|
assert.equal(challengeResponse.status, 200);
|
|
const challenge = (await challengeResponse.json() as { data: { challengeId: string; question: string } }).data;
|
|
const numbers = challenge.question.match(/(\d+) \+ (\d+)/); assert.ok(numbers);
|
|
const answer = String(Number(numbers[1]) + Number(numbers[2]));
|
|
const baseBody = { email, password: "password123", deviceId, captchaChallengeId: challenge.challengeId };
|
|
const wrong = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...baseBody, captchaAnswer: "0" }) });
|
|
assert.equal(wrong.status, 422);
|
|
const registered = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...baseBody, captchaAnswer: answer }) });
|
|
assert.equal(registered.status, 201);
|
|
const reused = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...baseBody, email: "captcha-second@example.com", captchaAnswer: answer }) });
|
|
assert.equal(reused.status, 422);
|
|
assert.equal(store.captchaChallenges.get(challenge.challengeId)?.consumedAt !== undefined, true);
|
|
} finally {
|
|
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("administrator MFA retry is exempt only after a valid CAPTCHA-bound challenge", async () => {
|
|
const secret = "JBSWY3DPEHPK3PXP";
|
|
const store = createStore();
|
|
const server = createHttpServer(store, { ...config(), captchaRequired: true, adminMfaRequired: true, adminMfaSecret: secret });
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
const address = server.address(); assert.ok(address && typeof address !== "string");
|
|
const base = `http://127.0.0.1:${address.port}`;
|
|
try {
|
|
const challengeResponse = await fetch(`${base}/api/v1/auth/challenge`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "admin-login", target: "admin@example.com", deviceId: "admin-browser" }) });
|
|
const challenge = (await challengeResponse.json() as { data: { challengeId: string; question: string } }).data; const numbers = challenge.question.match(/(\d+) \+ (\d+)/); assert.ok(numbers); const answer = String(Number(numbers[1]) + Number(numbers[2]));
|
|
const first = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now", deviceId: "admin-browser", captchaChallengeId: challenge.challengeId, captchaAnswer: answer }) });
|
|
const firstData = (await first.json() as { data: { mfaRequired?: boolean; challengeId?: string } }).data; assert.equal(first.status, 200); assert.equal(firstData.mfaRequired, true); assert.ok(firstData.challengeId);
|
|
const second = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now", deviceId: "admin-browser", challengeId: firstData.challengeId, mfaCode: totpCode(secret) }) });
|
|
const secondData = (await second.json() as { data: { accessToken?: string } }).data; assert.equal(second.status, 200); assert.ok(secondData.accessToken);
|
|
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); }
|
|
});
|
|
|
|
test("refresh restores a missing legacy CSRF cookie without weakening business writes", async () => {
|
|
const { store, server, base } = await start("super_admin");
|
|
try {
|
|
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
|
|
assert.equal(login.status, 200);
|
|
const setCookies = login.headers.getSetCookie();
|
|
const refresh = setCookies.find((value) => value.startsWith("refresh_admin="));
|
|
assert.ok(refresh);
|
|
const legacyCookie = refresh.split(";", 1)[0];
|
|
const refreshed = await fetch(`${base}/api/v1/admin/auth/refresh`, { method: "POST", headers: { cookie: `${legacyCookie}` } });
|
|
assert.equal(refreshed.status, 200);
|
|
const restoredCookies = refreshed.headers.getSetCookie();
|
|
const nextRefresh = restoredCookies.find((value) => value.startsWith("refresh_admin="));
|
|
assert.ok(nextRefresh);
|
|
assert.equal(restoredCookies.some((value) => value.startsWith("miragenflow_csrf_admin=") && !value.includes("HttpOnly")), true);
|
|
const refreshedPayload = await refreshed.json() as { data: { accessToken: string } };
|
|
const protectedWrite = await fetch(`${base}/api/v1/admin/settings`, { method: "PATCH", headers: { cookie: `${nextRefresh!.split(";", 1)[0]}`, authorization: `Bearer ${refreshedPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": "legacy-csrf-write" }, body: JSON.stringify({}) });
|
|
assert.equal(protectedWrite.status, 403);
|
|
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); }
|
|
});
|
|
|
|
async function start(role = "operator") {
|
|
const store = createStore();
|
|
const server = createHttpServer(store, config(role));
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
const address = server.address();
|
|
assert.ok(address && typeof address !== "string");
|
|
return { store, server, base: `http://127.0.0.1:${address.port}` };
|
|
}
|
|
|
|
test("admin RBAC rejects finance access and redacts support channel internals", async () => {
|
|
const { store, server, base } = await start("support");
|
|
try {
|
|
store.channels[0].providerName = "internal-provider";
|
|
store.channels[0].providerModelId = "internal-model";
|
|
store.channels[0].baseUrl = "https://provider.example.com";
|
|
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
|
|
const loginPayload = await login.json() as { data: { accessToken: string } };
|
|
assert.ok(loginPayload.data.accessToken);
|
|
const auth = { authorization: `Bearer ${loginPayload.data.accessToken}` };
|
|
const finance = await fetch(`${base}/api/v1/admin/billing/ledger`, { headers: auth });
|
|
assert.equal(finance.status, 403);
|
|
const channels = await fetch(`${base}/api/v1/admin/channels`, { headers: auth });
|
|
assert.equal(channels.status, 200);
|
|
const payload = await channels.json() as { data: { items: Array<Record<string, unknown>> } };
|
|
assert.equal(payload.data.items[0].providerModelId, undefined);
|
|
assert.equal(payload.data.items[0].baseUrl, undefined);
|
|
} finally {
|
|
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("payment provider credentials are encrypted and redacted", async () => {
|
|
const { store, server, base } = await start("super_admin");
|
|
try {
|
|
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
|
|
const loginPayload = await login.json() as { data: { accessToken: string } }; const auth = { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": "payment-provider-create" };
|
|
const created = await fetch(`${base}/api/v1/admin/payment-providers`, { method: "POST", headers: auth, body: JSON.stringify({ id: "secure-adapter", name: "Secure adapter", mode: "live", secret: "payment-secret-value" }) });
|
|
assert.equal(created.status, 201); const createdPayload = await created.json() as { data: { item: Record<string, unknown> } }; assert.equal(createdPayload.data.item.secretRef, undefined); assert.equal(createdPayload.data.item.secretConfigured, true);
|
|
const stored = store.paymentProviders.get("secure-adapter"); assert.ok(typeof stored?.secretRef === "string"); assert.notEqual(stored?.secretRef, "payment-secret-value");
|
|
const listed = await fetch(`${base}/api/v1/admin/payment-providers`, { headers: { authorization: `Bearer ${loginPayload.data.accessToken}` } }); const listedPayload = await listed.json() as { data: { items: Array<Record<string, unknown>> } }; assert.equal(listedPayload.data.items[0].secretRef, undefined); assert.equal(listedPayload.data.items[0].secret, undefined);
|
|
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); }
|
|
});
|
|
|
|
test("OpenAI image channel creation does not probe implicitly and explicit probing uses Bearer auth", async () => {
|
|
const { store, server, base } = await start("super_admin");
|
|
const previousFetch = globalThis.fetch;
|
|
const probeRequests: Array<{ method: string; authorization: string | null; url: string }> = [];
|
|
try {
|
|
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
|
|
const loginPayload = await login.json() as { data: { accessToken: string } };
|
|
globalThis.fetch = async (input, init) => {
|
|
const url = String(input);
|
|
if (url.startsWith("https://1.1.1.1/")) {
|
|
probeRequests.push({ url, method: init?.method || "GET", authorization: new Headers(init?.headers).get("authorization") });
|
|
return new Response(JSON.stringify({ data: [{ id: "gpt-image-2" }, { id: "gpt-image-2-2k" }] }), { status: 200, headers: { "content-type": "application/json" } });
|
|
}
|
|
return previousFetch(input, init);
|
|
};
|
|
const auth = { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json" };
|
|
const created = await fetch(`${base}/api/v1/admin/channels`, { method: "POST", headers: { ...auth, "idempotency-key": "openai-channel-create" }, body: JSON.stringify({ label: "OpenAI 图片渠道", providerType: "openai-images", providerModelId: "gpt-image-2", baseUrl: "https://1.1.1.1/v1", secretRef: "k", resolutionModelMap: { "1K": "gpt-image-2", "2K": "gpt-image-2-2k", invalid: "should-drop" } }) });
|
|
assert.equal(created.status, 201);
|
|
const createdPayload = await created.json() as { data: { item: Record<string, unknown> } };
|
|
assert.equal(createdPayload.data.item.secretRef, "configured");
|
|
assert.equal(createdPayload.data.item.baseUrl, "https://1.1.1.1/v1");
|
|
assert.deepEqual(createdPayload.data.item.resolutionModelMap, { "1K": "gpt-image-2", "2K": "gpt-image-2-2k" });
|
|
const channel = store.channels.find((item) => item.id === createdPayload.data.item.id);
|
|
assert.ok(channel);
|
|
assert.notEqual(channel.secretRef, "k");
|
|
assert.equal(probeRequests.length, 0);
|
|
|
|
const probe = await fetch(`${base}/api/v1/admin/channels/probe`, { method: "POST", headers: { ...auth, "idempotency-key": "openai-channel-probe" }, body: JSON.stringify({ providerType: "openai-images", baseUrl: "https://1.1.1.1/v1", secretRef: "k" }) });
|
|
assert.equal(probe.status, 200);
|
|
assert.equal(probeRequests[0]?.url, "https://1.1.1.1/v1/models");
|
|
assert.equal(probeRequests[0]?.method, "GET");
|
|
assert.equal(probeRequests[0]?.authorization, "Bearer k");
|
|
|
|
const invalid = await fetch(`${base}/api/v1/admin/channels`, { method: "POST", headers: { ...auth, "idempotency-key": "openai-channel-http" }, body: JSON.stringify({ label: "不安全渠道", providerType: "openai-images", providerModelId: "gpt-image-2", baseUrl: "http://1.1.1.1/v1", secretRef: "1234567890123456" }) });
|
|
assert.equal(invalid.status, 201);
|
|
assert.equal(store.channels.some((item) => item.label === "不安全渠道"), true);
|
|
} finally {
|
|
globalThis.fetch = previousFetch;
|
|
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("admin channel probe reports whether the upstream request was attempted and why it failed", async () => {
|
|
const { server, base } = await start("super_admin");
|
|
const previousFetch = globalThis.fetch;
|
|
try {
|
|
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
|
|
const loginPayload = await login.json() as { data: { accessToken: string } };
|
|
let upstreamRequest: { method?: string; authorization?: string | null } | undefined;
|
|
globalThis.fetch = async (input, init) => {
|
|
if (String(input) === "https://1.1.1.1/v1/models") {
|
|
upstreamRequest = { method: init?.method, authorization: new Headers(init?.headers).get("authorization") };
|
|
return new Response(JSON.stringify({ error: { message: "invalid api key" } }), { status: 401, headers: { "content-type": "application/json" } });
|
|
}
|
|
return previousFetch(input, init);
|
|
};
|
|
const response = await fetch(`${base}/api/v1/admin/channels/probe`, {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": "channel-probe-failure-details" },
|
|
body: JSON.stringify({ providerType: "openai-images", baseUrl: "https://1.1.1.1/v1", secretRef: "short-key" }),
|
|
});
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json() as { data: { item: { healthy: boolean; requestAttempted: boolean; requestPath: string; error?: string } } };
|
|
assert.equal(payload.data.item.healthy, false);
|
|
assert.equal(payload.data.item.requestAttempted, true);
|
|
assert.equal(payload.data.item.requestPath, "/v1/models");
|
|
assert.match(payload.data.item.error || "", /API Key/);
|
|
assert.deepEqual(upstreamRequest, { method: "GET", authorization: "Bearer short-key" });
|
|
} finally {
|
|
globalThis.fetch = previousFetch;
|
|
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("admin channel probe classifies transport causes without exposing credentials", async () => {
|
|
const { server, base } = await start("super_admin");
|
|
const previousFetch = globalThis.fetch;
|
|
const secret = "transport-secret-value";
|
|
try {
|
|
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
|
|
const loginPayload = await login.json() as { data: { accessToken: string } };
|
|
const cases = [
|
|
{ code: "ECONNREFUSED", text: "拒绝建立连接" },
|
|
{ code: "ECONNRESET", text: "连接被中断" },
|
|
{ code: "CERT_HAS_EXPIRED", text: "TLS" },
|
|
{ code: "UND_ERR_CONNECT_TIMEOUT", text: "超时" },
|
|
];
|
|
for (const [index, item] of cases.entries()) {
|
|
globalThis.fetch = async (input, init) => {
|
|
if (String(input) === "https://1.1.1.1/v1/models") throw Object.assign(new TypeError("fetch failed"), { cause: { code: item.code } });
|
|
return previousFetch(input, init);
|
|
};
|
|
const response = await fetch(`${base}/api/v1/admin/channels/probe`, { method: "POST", headers: { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": `transport-cause-${index}` }, body: JSON.stringify({ providerType: "openai-images", baseUrl: "https://1.1.1.1/v1", secretRef: secret }) });
|
|
const payload = await response.json() as { data: { item: { healthy: boolean; requestAttempted: boolean; error?: string } } };
|
|
assert.equal(response.status, 200);
|
|
assert.equal(payload.data.item.healthy, false);
|
|
assert.equal(payload.data.item.requestAttempted, true);
|
|
assert.match(payload.data.item.error || "", new RegExp(item.text));
|
|
assert.doesNotMatch(payload.data.item.error || "", /transport-secret-value|Bearer/);
|
|
}
|
|
} finally {
|
|
globalThis.fetch = previousFetch;
|
|
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("unsupported generic channel probes do not infer health from a configured key", async () => {
|
|
const { server, base } = await start("super_admin");
|
|
const previousFetch = globalThis.fetch;
|
|
let upstreamCalls = 0;
|
|
try {
|
|
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
|
|
const loginPayload = await login.json() as { data: { accessToken: string } };
|
|
globalThis.fetch = async (input, init) => {
|
|
if (String(input).startsWith("https://generic.example.com/")) upstreamCalls += 1;
|
|
return previousFetch(input, init);
|
|
};
|
|
const response = await fetch(`${base}/api/v1/admin/channels/probe`, {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": "generic-channel-probe" },
|
|
body: JSON.stringify({ providerType: "generic", baseUrl: "https://generic.example.com/v1", secretRef: "configured-key" }),
|
|
});
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json() as { data: { item: { healthy: boolean; requestAttempted: boolean; error?: string } } };
|
|
assert.equal(payload.data.item.healthy, false);
|
|
assert.equal(payload.data.item.requestAttempted, false);
|
|
assert.match(payload.data.item.error || "", /不支持自动模型列表探活/);
|
|
assert.equal(upstreamCalls, 0);
|
|
} finally {
|
|
globalThis.fetch = previousFetch;
|
|
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("provider probe reports blocked addresses before an upstream request", async () => {
|
|
const { server, base } = await start("super_admin");
|
|
const previousFetch = globalThis.fetch;
|
|
const previousAllowLocal = process.env.MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS;
|
|
let upstreamCalls = 0;
|
|
try {
|
|
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
|
|
const loginPayload = await login.json() as { data: { accessToken: string } };
|
|
process.env.MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS = "false";
|
|
globalThis.fetch = async (input, init) => {
|
|
if (String(input).startsWith("https://127.0.0.1")) upstreamCalls += 1;
|
|
return previousFetch(input, init);
|
|
};
|
|
const response = await fetch(`${base}/api/v1/admin/channels/probe`, {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": "dns-channel-probe" },
|
|
body: JSON.stringify({ providerType: "openai-images", baseUrl: "https://127.0.0.1/v1", secretRef: "configured-key" }),
|
|
});
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json() as { data: { item: { healthy: boolean; requestAttempted: boolean; error?: string } } };
|
|
assert.equal(payload.data.item.healthy, false);
|
|
assert.equal(payload.data.item.requestAttempted, false);
|
|
assert.match(payload.data.item.error || "", /不允许指向本机/);
|
|
assert.match(payload.data.item.error || "", /未发起|尚未发起/);
|
|
assert.equal(upstreamCalls, 0);
|
|
} finally {
|
|
globalThis.fetch = previousFetch;
|
|
if (previousAllowLocal === undefined) delete process.env.MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS;
|
|
else process.env.MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS = previousAllowLocal;
|
|
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("WebDAV If-Match mismatch returns a conflict before any remote request", async () => {
|
|
const { store, server, base } = await start();
|
|
try {
|
|
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "webdav-conflict@example.com", password: "password123" }) });
|
|
const registered = await register.json() as { data: { userId: string; devVerificationCode: string } };
|
|
await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
|
|
const login = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "webdav-conflict@example.com", password: "password123" }) });
|
|
const session = await login.json() as { data: { accessToken: string } };
|
|
const auth = { authorization: `Bearer ${session.data.accessToken}`, "content-type": "application/json" };
|
|
store.webdav.set(registered.data.userId, { userId: registered.data.userId, configured: true, directory: "miragenflow", state: "ready", retentionDays: 30, encryptedUrl: encryptSecret("https://dav.example.com", config().accessTokenSecret) });
|
|
store.webdavFiles.set(`${registered.data.userId}:canvas/manifest.json`, { userId: registered.data.userId, path: "canvas/manifest.json", mimeType: "application/json", data: "eA==", checksum: "a".repeat(64), etag: '"remote-v2"', version: 2, updatedAt: new Date().toISOString() });
|
|
const response = await fetch(`${base}/api/v1/me/webdav/file`, { method: "PUT", headers: auth, body: JSON.stringify({ path: "canvas/manifest.json", mimeType: "application/json", data: "eA==", ifMatch: '"stale"' }) });
|
|
assert.equal(response.status, 409);
|
|
const payload = await response.json() as { error?: { code?: string } };
|
|
assert.equal(payload.error?.code, "VERSION_CONFLICT");
|
|
assert.equal([...store.webdavJobs.values()].length, 0);
|
|
} finally {
|
|
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("WebDAV rejects traversal paths before creating a retry job", async () => {
|
|
const { store, server, base } = await start();
|
|
try {
|
|
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "webdav-path@example.com", password: "password123" }) });
|
|
const registered = await register.json() as { data: { userId: string; devVerificationCode: string } };
|
|
await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
|
|
const login = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "webdav-path@example.com", password: "password123" }) });
|
|
const session = await login.json() as { data: { accessToken: string } };
|
|
store.webdav.set(registered.data.userId, { userId: registered.data.userId, configured: true, directory: "miragenflow", state: "ready", retentionDays: 30, encryptedUrl: encryptSecret("https://dav.example.com", config().accessTokenSecret) });
|
|
const response = await fetch(`${base}/api/v1/me/webdav/file`, { method: "PUT", headers: { authorization: `Bearer ${session.data.accessToken}`, "content-type": "application/json" }, body: JSON.stringify({ path: "../escape", data: "eA==" }) });
|
|
assert.equal(response.status, 422);
|
|
assert.equal(store.webdavJobs.size, 0);
|
|
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); }
|
|
});
|
|
|
|
test("access tokens reject a signed token with an unexpected JOSE header", () => {
|
|
const token = issueAccessToken("user-1", "user", "header-test-secret", 900);
|
|
const [, body] = token.split(".");
|
|
const alteredHeader = Buffer.from(JSON.stringify({ alg: "HS512", typ: "JWT" })).toString("base64url");
|
|
const alteredBody = `${alteredHeader}.${body}`;
|
|
const alteredSignature = createHmac("sha256", "header-test-secret").update(alteredBody).digest("base64url");
|
|
assert.throws(() => verifyAccessToken(`${alteredBody}.${alteredSignature}`, "header-test-secret", "user"), /登录状态已失效/);
|
|
assert.doesNotThrow(() => verifyAccessToken(token, "header-test-secret", "user"));
|
|
});
|