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

195 lines
7.6 KiB
TypeScript

// 验证纪要服务凭据和 Mind 回调的失败关闭边界
import assert from "node:assert/strict";
import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import test from "node:test";
import { ONETALK_SUMMARY_READ_PURPOSE } from "@trade-message-center/onetalk-contract";
import {
createSummaryAuthorizationReader,
MIND_SUMMARY_AUTHORIZATION_PATH,
} from "../src/summary-authorization.ts";
const token = "summary-read-token-with-at-least-thirty-two-characters";
const request = {
purpose: ONETALK_SUMMARY_READ_PURPOSE,
workspaceId: "803937a7-f7d3-497d-a5ec-b99d0314669e",
channelAccountId: "account-1",
conversationId: "conversation-1",
} as const;
const response = (status: number, body: unknown): Response =>
({ status, json: async () => body }) as Response;
test("sends the exact summary scope and no Cookie to the fixed Mind callback", async () => {
let url = "";
let init: RequestInit | undefined;
const reader = createSummaryAuthorizationReader({
baseUrl: "https://mind.example.com",
timeoutMs: 100,
token,
fetch: async (input, requestInit) => {
url = String(input);
init = requestInit;
return response(200, {
purpose: ONETALK_SUMMARY_READ_PURPOSE,
scope: {
workspaceId: request.workspaceId,
channelAccountId: request.channelAccountId,
conversationId: request.conversationId,
},
permissions: ["read"],
});
},
});
const decision = await reader.authorize(request, `Bearer ${token}`, "request-1");
assert.equal(decision.allowed, true);
assert.equal(url, "https://mind.example.com" + MIND_SUMMARY_AUTHORIZATION_PATH);
assert.deepEqual(JSON.parse(String(init?.body)), request);
assert.equal((init?.headers as Record<string, string>).cookie, undefined);
assert.equal((init?.headers as Record<string, string>).authorization, `Bearer ${token}`);
assert.equal(init?.redirect, "error");
});
test("fails closed for missing config, bad credentials, invalid response, and scope mismatch", async () => {
const cases = [
{
config: { baseUrl: "https://mind.example.com", timeoutMs: 100 },
authorization: `Bearer ${token}`,
response: response(200, {}),
expected: "authorization_unavailable",
},
{
config: { baseUrl: "https://mind.example.com", timeoutMs: 100, token },
authorization: "Bearer wrong-token",
response: response(200, {}),
expected: "auth_required",
},
{
config: { baseUrl: "https://mind.example.com", timeoutMs: 100, token },
authorization: `Bearer ${token}`,
response: response(200, { purpose: ONETALK_SUMMARY_READ_PURPOSE }),
expected: "authorization_unavailable",
},
...(["workspaceId", "channelAccountId", "conversationId"] as const).map((field) => ({
config: { baseUrl: "https://mind.example.com", timeoutMs: 100, token },
authorization: `Bearer ${token}`,
response: response(200, {
purpose: ONETALK_SUMMARY_READ_PURPOSE,
scope: {
workspaceId: field === "workspaceId" ? "other-workspace" : request.workspaceId,
channelAccountId:
field === "channelAccountId" ? "other-account" : request.channelAccountId,
conversationId:
field === "conversationId" ? "other-conversation" : request.conversationId,
},
permissions: ["read"],
}),
expected: "authorization_unavailable",
})),
] as const;
for (const testCase of cases) {
const reader = createSummaryAuthorizationReader({
...testCase.config,
fetch: async () => testCase.response,
});
const decision = await reader.authorize(request, testCase.authorization, "request-1");
assert.deepEqual(decision, { allowed: false, code: testCase.expected });
}
});
test("uses the configured token68 grammar before comparing Bearer credentials", async () => {
let fetchCalls = 0;
const reader = createSummaryAuthorizationReader({
baseUrl: "https://mind.example.com",
timeoutMs: 100,
token,
fetch: async () => {
fetchCalls += 1;
return response(200, {});
},
});
for (const authorization of [
"Bearer summary-read-token-with an-internal-space-and-length",
"Bearer summary-read-token-with,comma-and-length-value",
"Bearer summary-read-token-with=padding-in-the-middle-value",
]) {
assert.deepEqual(await reader.authorize(request, authorization, "request-1"), {
allowed: false,
code: "auth_required",
});
}
assert.equal(fetchCalls, 0);
});
test("fails closed for real callback timeout, redirect, invalid JSON, and 500", async () => {
let mode: "timeout" | "redirect" | "invalid-json" | "server-error" = "timeout";
const callback = createServer((_request, response) => {
if (mode === "timeout") {
setTimeout(() => response.destroy(), 50);
return;
}
if (mode === "redirect") {
response.writeHead(302, { location: "/redirect-target" });
response.end();
return;
}
if (mode === "invalid-json") {
response.writeHead(200, { "content-type": "text/html" });
response.end("<html>not-json</html>");
return;
}
response.writeHead(500, { "content-type": "application/json" });
response.end(JSON.stringify({ code: "authorization_unavailable" }));
});
await new Promise<void>((resolve) => callback.listen(0, "127.0.0.1", resolve));
const port = (callback.address() as AddressInfo).port;
try {
for (const nextMode of ["timeout", "redirect", "invalid-json", "server-error"] as const) {
mode = nextMode;
const reader = createSummaryAuthorizationReader({
baseUrl: `http://127.0.0.1:${port}`,
timeoutMs: nextMode === "timeout" ? 5 : 100,
token,
});
assert.deepEqual(
await reader.authorize(request, `Bearer ${token}`, `request-${nextMode}`),
{ allowed: false, code: "authorization_unavailable" },
);
}
} finally {
await new Promise<void>((resolve, reject) =>
callback.close((error) => (error ? reject(error) : resolve())),
);
}
});
test("maps only exact Mind status and rejection pairs", async () => {
const cases = [
[401, "auth_required", "auth_required"],
[403, "scope_forbidden", "scope_mismatch"],
[403, "summary_workspace_disabled", "authorization_rejected"],
[400, "invalid_request", "authorization_rejected"],
[503, "authorization_unavailable", "authorization_unavailable"],
[403, "auth_required", "authorization_unavailable"],
[200, "scope_forbidden", "authorization_unavailable"],
] as const;
for (const [status, code, expected] of cases) {
const reader = createSummaryAuthorizationReader({
baseUrl: "https://mind.example.com",
timeoutMs: 100,
token,
fetch: async () => response(status, { code }),
});
assert.deepEqual(await reader.authorize(request, `Bearer ${token}`, "request-1"), {
allowed: false,
code: expected,
});
}
});