Files
trade-message-center/apps/server/test/mind-authorization.test.ts
T

373 lines
13 KiB
TypeScript

// 验证 Mind 两个固定授权端点的请求边界和严格失败关闭
import assert from "node:assert/strict";
import test from "node:test";
import {
createMindAuthorizationReader,
type MindAuthorizationClientConfig,
} from "../src/mind-authorization.ts";
import { createOneTalkCutoverPolicy } from "../src/cutover-policy.ts";
import { ONETALK_PROTOCOL_VERSION } from "@trade-message-center/onetalk-contract";
const scope = {
mindUserId: "mind-user-1",
workspaceId: "workspace-1",
channelAccountId: "account-1",
};
const response = (status: number, body: unknown): Response =>
({
ok: status >= 200 && status < 300,
status,
json: async () => body,
}) as Response;
test("sends only binding fields to Mind binding authorization", async () => {
let request: RequestInit | undefined;
const reader = createMindAuthorizationReader({
baseUrl: "https://mind.example.com",
timeoutMs: 100,
fetch: async (_input, init) => {
request = init;
return response(200, {
binding: "binding-1",
authorizationVersion: "version-1",
permissions: ["read", "send"],
mindScope: scope,
});
},
});
const decision = await reader.authorize({
connectionType: "plugin",
operation: "connect",
scope: { channelAccountId: "account-1", deviceId: "device-1" },
binding: "binding-1",
});
assert.equal(decision.allowed, true);
assert.deepEqual(JSON.parse(String(request?.body)), {
channelAccountId: "account-1",
binding: "binding-1",
});
assert.equal((request?.headers as Record<string, string>).cookie, undefined);
});
test("does not expose raw Mind authorization payload hooks", () => {
const requestConfig = {
baseUrl: "https://mind.example.com",
timeoutMs: 100,
// @ts-expect-error Raw request payload callbacks are not a safe auth boundary.
onRequestPayload: () => {},
} satisfies MindAuthorizationClientConfig;
const responseConfig = {
baseUrl: "https://mind.example.com",
timeoutMs: 100,
// @ts-expect-error Raw response body callbacks are not a safe auth boundary.
onResponseResult: () => {},
} satisfies MindAuthorizationClientConfig;
void requestConfig;
void responseConfig;
});
test("emits safe diagnostics for Mind authorization responses and transport failures", async () => {
const events: unknown[] = [];
const rejectedReader = createMindAuthorizationReader({
baseUrl: "https://mind.example.com",
timeoutMs: 100,
onDiagnostic: (event) => events.push(event),
fetch: async () => response(403, { code: "binding_revoked" }),
});
await rejectedReader.authorize({
connectionType: "plugin",
operation: "connect",
scope: { channelAccountId: "account-1", deviceId: "device-1" },
binding: "binding-1",
});
const unavailableReader = createMindAuthorizationReader({
baseUrl: "https://mind.example.com",
timeoutMs: 100,
onDiagnostic: (event) => events.push(event),
fetch: async () => {
throw new Error("transport details must not be logged");
},
});
await unavailableReader.authorize({
connectionType: "mind_page",
operation: "read",
scope: { channelAccountId: "account-1" },
cookie: "secret-session",
});
assert.deepEqual(events, [
{
event: "mind_authorization",
endpoint: "binding",
operation: "connect",
outcome: "request_started",
},
{
event: "mind_authorization",
endpoint: "binding",
operation: "connect",
outcome: "rejected",
status: 403,
code: "binding_revoked",
},
{
event: "mind_authorization",
endpoint: "session",
operation: "read",
outcome: "request_started",
},
{
event: "mind_authorization",
endpoint: "session",
operation: "read",
outcome: "request_failed",
code: "authorization_unavailable",
},
]);
assert.equal(JSON.stringify(events).includes("secret-session"), false);
});
test("forwards the original Cookie only to session authorization", async () => {
let url = "";
let request: RequestInit | undefined;
const reader = createMindAuthorizationReader({
baseUrl: "https://mind.example.com",
timeoutMs: 100,
fetch: async (input, init) => {
url = String(input);
request = init;
return response(200, {
binding: "binding-1",
authorizationVersion: "version-1",
permissions: ["read"],
mindScope: scope,
});
},
});
const decision = await reader.authorize({
connectionType: "mind_page",
operation: "read",
scope: { channelAccountId: "account-1" },
cookie: "mind_session=opaque",
});
assert.equal(decision.allowed, true);
assert.match(url, /authorize-session$/);
assert.equal((request?.headers as Record<string, string>).cookie, "mind_session=opaque");
assert.deepEqual(JSON.parse(String(request?.body)), { channelAccountId: "account-1" });
});
test("maps malformed, unknown and transport failures to unavailable", async () => {
for (const result of [
response(200, { binding: "binding-1" }),
response(403, { code: "unknown_code" }),
null,
]) {
const reader = createMindAuthorizationReader({
baseUrl: "https://mind.example.com",
timeoutMs: 100,
fetch: async () => {
if (result === null) throw new Error("network");
return result;
},
});
const decision = await reader.authorize({
connectionType: "plugin",
operation: "connect",
scope: { channelAccountId: "account-1", deviceId: "device-1" },
binding: "binding-1",
});
assert.deepEqual(decision, { allowed: false, code: "authorization_unavailable" });
}
});
test("enforces exact Mind status-code pairs and transport failure boundaries", async () => {
const validBody = {
binding: "binding-1",
authorizationVersion: "version-1",
permissions: ["read"],
mindScope: scope,
};
const cases = [
{
input: {
connectionType: "plugin" as const,
operation: "connect" as const,
scope: { channelAccountId: "account-1", deviceId: "device-1" },
binding: "binding-1",
},
status: 200,
body: validBody,
expected: { allowed: true as const, ...validBody },
},
{
input: {
connectionType: "plugin" as const,
operation: "connect" as const,
scope: { channelAccountId: "account-1", deviceId: "device-1" },
binding: "binding-1",
},
status: 403,
body: { code: "binding_revoked" },
expected: { allowed: false as const, code: "binding_revoked" as const },
},
{
input: {
connectionType: "plugin" as const,
operation: "connect" as const,
scope: { channelAccountId: "account-1", deviceId: "device-1" },
binding: "binding-1",
},
status: 401,
body: { code: "binding_revoked" },
expected: { allowed: false as const, code: "authorization_unavailable" as const },
},
{
input: {
connectionType: "mind_page" as const,
operation: "read" as const,
scope: { channelAccountId: "account-1" },
cookie: "mind_session=opaque",
},
status: 401,
body: { code: "auth_required" },
expected: { allowed: false as const, code: "auth_required" as const },
},
{
input: {
connectionType: "mind_page" as const,
operation: "read" as const,
scope: { channelAccountId: "account-1" },
cookie: "mind_session=opaque",
},
status: 403,
body: { code: "scope_mismatch" },
expected: { allowed: false as const, code: "scope_mismatch" as const },
},
{
input: {
connectionType: "mind_page" as const,
operation: "read" as const,
scope: { channelAccountId: "account-1" },
cookie: "mind_session=opaque",
},
status: 403,
body: { code: "binding_revoked" },
expected: { allowed: false as const, code: "authorization_unavailable" as const },
},
{
input: {
connectionType: "plugin" as const,
operation: "connect" as const,
scope: { channelAccountId: "account-1", deviceId: "device-1" },
binding: "binding-1",
},
status: 500,
body: { code: "binding_revoked" },
expected: { allowed: false as const, code: "authorization_unavailable" as const },
},
{
input: {
connectionType: "plugin" as const,
operation: "connect" as const,
scope: { channelAccountId: "account-1", deviceId: "device-1" },
binding: "binding-1",
},
status: 200,
body: { code: "auth_required" },
expected: { allowed: false as const, code: "authorization_unavailable" as const },
},
] as const;
for (const testCase of cases) {
let request: RequestInit | undefined;
const reader = createMindAuthorizationReader({
baseUrl: "https://mind.example.com",
timeoutMs: 100,
fetch: async (_input, init) => {
request = init;
return response(testCase.status, testCase.body);
},
});
const decision = await reader.authorize(testCase.input);
assert.deepEqual(decision, testCase.expected);
assert.equal(request?.method, "POST");
assert.equal(request?.redirect, "error");
assert.equal(
(request?.headers as Record<string, string>)?.["content-type"],
"application/json",
);
}
const nonJsonReader = createMindAuthorizationReader({
baseUrl: "https://mind.example.com",
timeoutMs: 100,
fetch: async () =>
({
status: 200,
json: async () => {
throw new SyntaxError("not json");
},
}) as unknown as Response,
});
assert.deepEqual(
await nonJsonReader.authorize({
connectionType: "plugin",
operation: "connect",
scope: { channelAccountId: "account-1", deviceId: "device-1" },
binding: "binding-1",
}),
{ allowed: false, code: "authorization_unavailable" },
);
const timeoutReader = createMindAuthorizationReader({
baseUrl: "https://mind.example.com",
timeoutMs: 1,
fetch: async (_input, init) =>
new Promise((_resolve, reject) => {
const timer = setTimeout(() => reject(new Error("timeout")), 100);
if (init?.signal?.aborted) {
clearTimeout(timer);
reject(new Error("aborted"));
return;
}
init?.signal?.addEventListener(
"abort",
() => {
clearTimeout(timer);
reject(new Error("aborted"));
},
{ once: true },
);
}),
});
assert.deepEqual(
await timeoutReader.authorize({
connectionType: "plugin",
operation: "connect",
scope: { channelAccountId: "account-1", deviceId: "device-1" },
binding: "binding-1",
}),
{ allowed: false, code: "authorization_unavailable" },
);
});
test("cutover pause is fail-closed and Bright v6 can resume", () => {
const policy = createOneTalkCutoverPolicy();
assert.equal(policy.canAdmit("bright-v6", "plugin", ONETALK_PROTOCOL_VERSION), true);
policy.pause();
assert.equal(policy.canAdmit("bright-v6", "plugin", ONETALK_PROTOCOL_VERSION), false);
assert.equal(policy.resume(), true);
policy.pause();
assert.equal(policy.resume(), true);
assert.equal(policy.snapshot().paused, false);
assert.equal(policy.canAdmit("bright-v6", "plugin", ONETALK_PROTOCOL_VERSION), true);
assert.equal(policy.canAdmit("bright-v6", "plugin", 5), false);
assert.equal(policy.canAdmit("legacy", "plugin", 3), false);
});