mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
feat(mind-http-mock): log accepted contact profiles
This commit is contained in:
@@ -10,3 +10,5 @@ apps/*/dist/
|
||||
apps/chrome-extension/.keys/
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
mock-log.text
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user