mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
feat(mind-test-harness): support multiple mock channel accounts
This commit is contained in:
@@ -14,11 +14,13 @@ MIND_MOCK_PORT=8787
|
||||
MIND_TEST_HARNESS_HOST=127.0.0.1
|
||||
MIND_TEST_HARNESS_PORT=8788
|
||||
MIND_TEST_HARNESS_BRIGHT_BASE_URL=http://127.0.0.1:7878
|
||||
MIND_MOCK_CHANNEL_ACCOUNT_ID=243340382,286995452
|
||||
```
|
||||
|
||||
- `src/entry.ts` 必须启动 Mind 授权模拟监听器和 Bright 联调页监听器。
|
||||
- `pnpm dev` 不得启动、等待或依赖该包;只有显式 `pnpm dev:harness` 才运行它。
|
||||
- 联调页使用 `MIND_TEST_HARNESS_BRIGHT_BASE_URL` 请求 Bright HTTP 和 WS;不能再由 `apps/server` 的 `/harness` 路由提供。
|
||||
- `MIND_MOCK_CHANNEL_ACCOUNT_ID` 使用逗号分隔的非空唯一账号列表;Mind mock 对列表内账号授权,并在成功响应中回显本次请求的单个 `channelAccountId`。
|
||||
|
||||
## 3. Contracts
|
||||
|
||||
@@ -34,6 +36,7 @@ MIND_TEST_HARNESS_BRIGHT_BASE_URL=http://127.0.0.1:7878
|
||||
| 条件 | 结果 |
|
||||
| --- | --- |
|
||||
| `MIND_TEST_HARNESS_PORT` 或 `MIND_MOCK_PORT` 非 0–65535 整数 | 启动前抛出对应环境变量错误 |
|
||||
| `MIND_MOCK_CHANNEL_ACCOUNT_ID` 含空项或重复账号 | 启动前抛出对应环境变量错误 |
|
||||
| Bright base URL 不是精确 HTTP/HTTPS origin | 启动前抛出 `Invalid MIND_TEST_HARNESS_BRIGHT_BASE_URL` |
|
||||
| 监听端口已被占用 | 启动失败并关闭已打开的另一监听器 |
|
||||
| 非 `GET /` 请求联调页监听器 | 返回 404 |
|
||||
|
||||
@@ -134,7 +134,7 @@ Bright 不连接 Mind 数据库、不读取认证视图、不共享 Mind 登录
|
||||
|
||||
### Mind 本地测试支架
|
||||
|
||||
`mind-test-harness` 是仅供手工测试的独立开发工具:它模拟两个 Mind 授权接口,并提供 Bright HTTP/WS 联调页。Mind 模拟默认监听 `127.0.0.1:8787`,默认 fixture 是账号 `286995452`、binding `binding-123` 和 Cookie `mind_session=mock-valid`;通过 `MIND_MOCK_*` 环境变量覆盖 fixture 后重启即可模拟 binding/version/cookie 切换。两个 endpoint 还可独立设置结果:`MIND_MOCK_BINDING_RESULT=allow|scope_mismatch|binding_revoked|authorization_unavailable`,`MIND_MOCK_SESSION_RESULT=allow|auth_required|scope_mismatch|authorization_rejected|authorization_unavailable`。
|
||||
`mind-test-harness` 是仅供手工测试的独立开发工具:它模拟两个 Mind 授权接口,并提供 Bright HTTP/WS 联调页。Mind 模拟默认监听 `127.0.0.1:8787`,默认 fixture 是账号 `243340382`、`286995452`、binding `binding-123` 和 Cookie `mind_session=mock-valid`;`MIND_MOCK_CHANNEL_ACCOUNT_ID` 支持逗号分隔的账号列表,设置 `MIND_MOCK_*` 环境变量后重启即可模拟 binding/version/cookie 切换。两个 endpoint 还可独立设置结果:`MIND_MOCK_BINDING_RESULT=allow|scope_mismatch|binding_revoked|authorization_unavailable`,`MIND_MOCK_SESSION_RESULT=allow|auth_required|scope_mismatch|authorization_rejected|authorization_unavailable`。
|
||||
|
||||
根目录执行 `pnpm dev` 时不会启动支架;需要手工验证时才另开终端在开发环境运行 `NODE_ENV=development pnpm dev:harness`。支架不被任何 workspace 包依赖,不含测试文件,不声明 build 脚本,也不进入根 `dev`、`typecheck`、`test`、`build` 或生产 Docker 镜像。支架错误只在其独立命令中报告,不能改变主流程结果。
|
||||
|
||||
@@ -274,7 +274,7 @@ curl http://127.0.0.1:7878/health
|
||||
http://127.0.0.1:8788/
|
||||
```
|
||||
|
||||
本地 harness 中的 OneTalk account 应与 `MIND_MOCK_CHANNEL_ACCOUNT_ID`(默认 `286995452`)一致;生产 Mind 页面不提交可信的 user/workspace header,而由 Cookie Session 授权结果派生。页面会连接配置的 Bright 地址的 `/ws/mind`,插件使用 Bright 的 `/ws/plugin`;`MIND_PAGE_ORIGIN` 必须设为该页面的 Origin。
|
||||
本地 harness 中的 OneTalk account 应包含在 `MIND_MOCK_CHANNEL_ACCOUNT_ID`(默认 `243340382,286995452`)中;生产 Mind 页面不提交可信的 user/workspace header,而由 Cookie Session 授权结果派生。页面会连接配置的 Bright 地址的 `/ws/mind`,插件使用 Bright 的 `/ws/plugin`;`MIND_PAGE_ORIGIN` 必须设为该页面的 Origin。
|
||||
|
||||
## 加载和配置 Chrome 扩展
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
|
||||
import { isPlainRecord } from "@trade-message-center/onetalk-contract";
|
||||
|
||||
import type { MindMockAuthorization, MindMockBindingResult } from "../model.ts";
|
||||
import {
|
||||
authorizationForChannelAccount,
|
||||
type MindMockAuthorization,
|
||||
type MindMockBindingResult,
|
||||
} from "../model.ts";
|
||||
import { hasExactKeys, isJsonRequest, isNonEmptyString, readBody } from "../request.ts";
|
||||
import { writeAuthorization, writePlainResponse, writeRejection } from "../response.ts";
|
||||
|
||||
@@ -39,6 +43,7 @@ export const handleBindingAuthorization = async (
|
||||
request: IncomingMessage,
|
||||
response: ServerResponse,
|
||||
authorization: MindMockAuthorization,
|
||||
channelAccountIds: readonly string[],
|
||||
result: MindMockBindingResult,
|
||||
): Promise<void> => {
|
||||
if (!isJsonRequest(request)) {
|
||||
@@ -58,7 +63,7 @@ export const handleBindingAuthorization = async (
|
||||
return;
|
||||
}
|
||||
if (writeBindingResult(response, result)) return;
|
||||
if (input.channelAccountId !== authorization.mindScope.channelAccountId) {
|
||||
if (!channelAccountIds.includes(input.channelAccountId)) {
|
||||
writeRejection(response, 403, "scope_mismatch");
|
||||
return;
|
||||
}
|
||||
@@ -66,5 +71,8 @@ export const handleBindingAuthorization = async (
|
||||
writeRejection(response, 403, "binding_revoked");
|
||||
return;
|
||||
}
|
||||
writeAuthorization(response, authorization);
|
||||
writeAuthorization(
|
||||
response,
|
||||
authorizationForChannelAccount(authorization, input.channelAccountId),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,11 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
|
||||
import { isPlainRecord } from "@trade-message-center/onetalk-contract";
|
||||
|
||||
import type { MindMockAuthorization, MindMockSessionResult } from "../model.ts";
|
||||
import {
|
||||
authorizationForChannelAccount,
|
||||
type MindMockAuthorization,
|
||||
type MindMockSessionResult,
|
||||
} from "../model.ts";
|
||||
import { hasExactKeys, isJsonRequest, isNonEmptyString, readBody } from "../request.ts";
|
||||
import { writeAuthorization, writePlainResponse, writeRejection } from "../response.ts";
|
||||
|
||||
@@ -39,6 +43,7 @@ export const handleSessionAuthorization = async (
|
||||
request: IncomingMessage,
|
||||
response: ServerResponse,
|
||||
authorization: MindMockAuthorization,
|
||||
channelAccountIds: readonly string[],
|
||||
cookie: string,
|
||||
result: MindMockSessionResult,
|
||||
): Promise<void> => {
|
||||
@@ -63,9 +68,12 @@ export const handleSessionAuthorization = async (
|
||||
writeRejection(response, 401, "auth_required");
|
||||
return;
|
||||
}
|
||||
if (input.channelAccountId !== authorization.mindScope.channelAccountId) {
|
||||
if (!channelAccountIds.includes(input.channelAccountId)) {
|
||||
writeRejection(response, 403, "scope_mismatch");
|
||||
return;
|
||||
}
|
||||
writeAuthorization(response, authorization);
|
||||
writeAuthorization(
|
||||
response,
|
||||
authorizationForChannelAccount(authorization, input.channelAccountId),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ONETALK_PERMISSIONS } from "@trade-message-center/onetalk-contract";
|
||||
|
||||
const DEFAULT_HOST = "127.0.0.1";
|
||||
const DEFAULT_PORT = 8787;
|
||||
const DEFAULT_CHANNEL_ACCOUNT_ID = "243340382";
|
||||
const DEFAULT_CHANNEL_ACCOUNT_ID = ["243340382", "286995452"] as const;
|
||||
const DEFAULT_BINDING = "123";
|
||||
const DEFAULT_AUTHORIZATION_VERSION = "mock-v1";
|
||||
const DEFAULT_MIND_USER_ID = "mind-user-1";
|
||||
@@ -29,6 +29,7 @@ export type MindMockConfig = {
|
||||
port: number;
|
||||
cookie: string;
|
||||
authorization: MindMockAuthorization;
|
||||
channelAccountIds: readonly string[];
|
||||
bindingResult?: MindMockBindingResult;
|
||||
sessionResult?: MindMockSessionResult;
|
||||
};
|
||||
@@ -84,18 +85,36 @@ const parsePermissions = (value: string): MindMockPermission[] => {
|
||||
return permissions as MindMockPermission[];
|
||||
};
|
||||
|
||||
const parseFixtureString = (
|
||||
environment: Record<string, string | undefined>,
|
||||
key: string,
|
||||
defaultValue: string,
|
||||
): string => {
|
||||
const value = readEnvironmentValue(environment, key, defaultValue);
|
||||
const validateFixtureString = (value: string, key: string): string => {
|
||||
if (value.includes("\r") || value.includes("\n")) {
|
||||
throw new Error(`Invalid ${key}: line breaks are not allowed`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const parseFixtureString = (
|
||||
environment: Record<string, string | undefined>,
|
||||
key: string,
|
||||
defaultValue: string,
|
||||
): string => validateFixtureString(readEnvironmentValue(environment, key, defaultValue), key);
|
||||
|
||||
const parseFixtureStringList = (
|
||||
environment: Record<string, string | undefined>,
|
||||
key: string,
|
||||
defaultValues: readonly string[],
|
||||
): string[] => {
|
||||
const value = readEnvironmentValue(environment, key, defaultValues.join(","));
|
||||
const values = value.split(",").map((item) => item.trim());
|
||||
if (
|
||||
values.length === 0 ||
|
||||
values.some((item) => item === "") ||
|
||||
new Set(values).size !== values.length
|
||||
) {
|
||||
throw new Error(`Invalid ${key}: expected unique non-empty values`);
|
||||
}
|
||||
return values.map((item) => validateFixtureString(item, key));
|
||||
};
|
||||
|
||||
const parseResult = <T extends string>(
|
||||
environment: Record<string, string | undefined>,
|
||||
key: string,
|
||||
@@ -112,6 +131,11 @@ const parseResult = <T extends string>(
|
||||
export const parseMindMockConfig = (
|
||||
environment: Record<string, string | undefined> = process.env,
|
||||
): MindMockConfig => {
|
||||
const channelAccountIds = parseFixtureStringList(
|
||||
environment,
|
||||
"MIND_MOCK_CHANNEL_ACCOUNT_ID",
|
||||
DEFAULT_CHANNEL_ACCOUNT_ID,
|
||||
);
|
||||
const authorization: MindMockAuthorization = {
|
||||
binding: parseFixtureString(environment, "MIND_MOCK_BINDING", DEFAULT_BINDING),
|
||||
authorizationVersion: parseFixtureString(
|
||||
@@ -133,11 +157,7 @@ export const parseMindMockConfig = (
|
||||
"MIND_MOCK_WORKSPACE_ID",
|
||||
DEFAULT_WORKSPACE_ID,
|
||||
),
|
||||
channelAccountId: parseFixtureString(
|
||||
environment,
|
||||
"MIND_MOCK_CHANNEL_ACCOUNT_ID",
|
||||
DEFAULT_CHANNEL_ACCOUNT_ID,
|
||||
),
|
||||
channelAccountId: channelAccountIds[0]!,
|
||||
},
|
||||
};
|
||||
return {
|
||||
@@ -148,6 +168,7 @@ export const parseMindMockConfig = (
|
||||
),
|
||||
cookie: parseFixtureString(environment, "MIND_MOCK_COOKIE", DEFAULT_COOKIE),
|
||||
authorization,
|
||||
channelAccountIds,
|
||||
bindingResult: parseResult(environment, "MIND_MOCK_BINDING_RESULT", BINDING_MOCK_RESULTS),
|
||||
sessionResult: parseResult(environment, "MIND_MOCK_SESSION_RESULT", SESSION_MOCK_RESULTS),
|
||||
};
|
||||
|
||||
@@ -15,6 +15,14 @@ export type MindMockAuthorization = {
|
||||
};
|
||||
};
|
||||
|
||||
export const authorizationForChannelAccount = (
|
||||
authorization: MindMockAuthorization,
|
||||
channelAccountId: string,
|
||||
): MindMockAuthorization => ({
|
||||
...authorization,
|
||||
mindScope: { ...authorization.mindScope, channelAccountId },
|
||||
});
|
||||
|
||||
export const BINDING_MOCK_RESULTS = [
|
||||
"allow",
|
||||
"scope_mismatch",
|
||||
|
||||
@@ -38,6 +38,7 @@ const handleRequest = async (
|
||||
request,
|
||||
response,
|
||||
config.authorization,
|
||||
config.channelAccountIds,
|
||||
config.bindingResult ?? "allow",
|
||||
);
|
||||
return;
|
||||
@@ -47,6 +48,7 @@ const handleRequest = async (
|
||||
request,
|
||||
response,
|
||||
config.authorization,
|
||||
config.channelAccountIds,
|
||||
config.cookie,
|
||||
config.sessionResult ?? "allow",
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user