feat(mind-http-mock): log accepted contact profiles

This commit is contained in:
YBF
2026-09-01 18:45:27 +08:00
parent 89d8240505
commit 204e32a5ae
5 changed files with 89 additions and 6 deletions
+2
View File
@@ -10,3 +10,5 @@ apps/*/dist/
apps/chrome-extension/.keys/
*.log
.DS_Store
mock-log.text
+1 -1
View File
@@ -11,7 +11,7 @@ import {
const DEFAULT_HOST = "127.0.0.1";
const DEFAULT_PORT = 8787;
const DEFAULT_CHANNEL_ACCOUNT_ID = "243340382";
const DEFAULT_CHANNEL_ACCOUNT_ID = "286995452";
const DEFAULT_BINDING = "binding-123";
const DEFAULT_AUTHORIZATION_VERSION = "mock-v1";
const DEFAULT_MIND_USER_ID = "mind-user-1";
@@ -1,5 +1,7 @@
// 校验并接收 Bright 的本地 profile receipt
import { appendFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import type { IncomingMessage, ServerResponse } from "node:http";
import {
@@ -16,18 +18,22 @@ import type { MindContactProfileStore } from "./store.ts";
export const MIND_CONTACT_PROFILE_PATH = "/internal/bright/onetalk/contact-profiles";
type ContactProfileReceipt = {
export type MindContactProfileReceipt = {
channelAccountId: string;
binding: string;
profiles: OneTalkContactProfile[];
};
export type MindContactProfileReceiptSink = (receipt: MindContactProfileReceipt) => Promise<void>;
const mockLogPath = fileURLToPath(new URL("./mock-log.text", import.meta.url));
const isApprovedProfile = (value: unknown): value is OneTalkContactProfile => {
if (!isOneTalkContactProfile(value)) return false;
return value.avatarUrl === null || isOneTalkAvatarUrl(value.avatarUrl);
};
const readContactProfileReceipt = (body: unknown): ContactProfileReceipt | null => {
const readContactProfileReceipt = (body: unknown): MindContactProfileReceipt | null => {
if (
!isRecord(body) ||
!hasExactKeys(body, ["channelAccountId", "binding", "profiles"]) ||
@@ -47,12 +53,18 @@ const readContactProfileReceipt = (body: unknown): ContactProfileReceipt | null
};
};
/** 将已通过 mock 授权的 profile receipt 逐行写入本地开发日志。 */
export const appendContactProfileMockLog: MindContactProfileReceiptSink = async (receipt) => {
await appendFile(mockLogPath, `${JSON.stringify(receipt)}\n`, "utf8");
};
/** 接收只含白名单 profile 的 Bright 本地 receipt,并覆盖已存头像值。 */
export const handleContactProfileReceipt = async (
request: IncomingMessage,
response: ServerResponse,
authorization: MindMockAuthorization,
store: MindContactProfileStore,
receiptSink: MindContactProfileReceiptSink = appendContactProfileMockLog,
): Promise<void> => {
if (!isJsonRequest(request)) {
writeRejection(response, 400, "invalid_request");
@@ -81,6 +93,12 @@ export const handleContactProfileReceipt = async (
return;
}
try {
await receiptSink(input);
} catch {
writeRejection(response, 500, "mock_log_unavailable");
return;
}
store.upsert(input.channelAccountId, input.profiles);
writeNoContentResponse(response);
};
+14 -2
View File
@@ -8,8 +8,10 @@ import {
} from "./authorization/binding.ts";
import { parseMindMockConfig, type MindMockConfig } from "./config.ts";
import {
appendContactProfileMockLog,
handleContactProfileReceipt,
MIND_CONTACT_PROFILE_PATH,
type MindContactProfileReceiptSink,
} from "./contact-profile/handler.ts";
import {
createMindContactProfileStore,
@@ -40,6 +42,7 @@ export type {
export type MindMockDependencies = {
contactProfileStore?: MindContactProfileStore;
contactProfileReceiptSink?: MindContactProfileReceiptSink;
};
const handleRequest = async (
@@ -47,6 +50,7 @@ const handleRequest = async (
response: ServerResponse,
config: MindMockConfig,
profileStore: MindContactProfileStore,
profileReceiptSink: MindContactProfileReceiptSink,
): Promise<void> => {
const path = readPath(request);
if (request.method !== "POST") {
@@ -73,7 +77,13 @@ const handleRequest = async (
return;
}
if (path === MIND_CONTACT_PROFILE_PATH) {
await handleContactProfileReceipt(request, response, config.authorization, profileStore);
await handleContactProfileReceipt(
request,
response,
config.authorization,
profileStore,
profileReceiptSink,
);
return;
}
writePlainResponse(response, 404, "Not Found");
@@ -85,7 +95,9 @@ export const createMindAuthorizationServer = (
dependencies: MindMockDependencies = {},
): Server => {
const profileStore = dependencies.contactProfileStore ?? createMindContactProfileStore();
const profileReceiptSink =
dependencies.contactProfileReceiptSink ?? appendContactProfileMockLog;
return createServer((request, response) => {
void handleRequest(request, response, config, profileStore);
void handleRequest(request, response, config, profileStore, profileReceiptSink);
});
};
+52 -1
View File
@@ -50,7 +50,10 @@ const startServer = async (
config: MindMockConfig,
dependencies: MindMockDependencies = {},
): Promise<{ url: string; close: () => Promise<void> }> => {
const server = createMindAuthorizationServer(config, dependencies);
const server = createMindAuthorizationServer(config, {
contactProfileReceiptSink: async () => {},
...dependencies,
});
server.listen(config.port, config.host);
await once(server, "listening");
const address = server.address() as AddressInfo;
@@ -217,6 +220,54 @@ test("retains URL and clears null in the account-scoped profile store over TCP",
}
});
test("writes each accepted profile receipt to the injected mock log sink", async (context) => {
const receipts: unknown[] = [];
const running = await startServer(testConfig(), {
contactProfileReceiptSink: async (receipt) => {
receipts.push(receipt);
},
});
closeAfterTest(context, running.close);
const received = await fetchResponse(
`${running.url}${MIND_CONTACT_PROFILE_PATH}`,
jsonOptions({
channelAccountId: "account-1",
binding: "binding-1",
profiles: [profile],
}),
);
assert.equal(received.status, 204);
assert.deepEqual(receipts, [
{
channelAccountId: "account-1",
binding: "binding-1",
profiles: [profile],
},
]);
});
test("does not log rejected profile receipts", async (context) => {
const receipts: unknown[] = [];
const running = await startServer(testConfig(), {
contactProfileReceiptSink: async (receipt) => {
receipts.push(receipt);
},
});
closeAfterTest(context, running.close);
const rejected = await fetchResponse(
`${running.url}${MIND_CONTACT_PROFILE_PATH}`,
jsonOptions({
channelAccountId: "account-1",
binding: "wrong-binding",
profiles: [profile],
}),
);
assert.equal(rejected.status, 403);
assert.deepEqual(receipts, []);
});
test("isolates profile receipts by account and binding over TCP", async (context) => {
const store = createMindContactProfileStore();
const accountOne = await startServer(testConfig(), { contactProfileStore: store });