mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
351 lines
12 KiB
JavaScript
351 lines
12 KiB
JavaScript
// 验证当前会话历史同步的安全分页行为
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { installCurrentConversationHistorySync } from "../src/onetalk/main-page/current-conversation-history/entry.ts";
|
|
import { syncCurrentConversationHistory } from "../src/onetalk/main-page/current-conversation-history/index.ts";
|
|
import { isAccountId } from "../src/onetalk/main-page/model.ts";
|
|
|
|
const targetAccountId = 2500002169502;
|
|
const targetConversation = {
|
|
cid: "conversation-1",
|
|
accountId: targetAccountId,
|
|
accountIdEncrypt: "encrypted-contact-account",
|
|
aliId: "seller@icbu",
|
|
aliIdEncrypt: "encrypted-seller-account",
|
|
};
|
|
|
|
function historyMessage(messageId, sendTime) {
|
|
return { messageId, sendTime };
|
|
}
|
|
|
|
function createPageWindow({ conversationPages, historyPages }) {
|
|
const conversationCalls = [];
|
|
const historyCalls = [];
|
|
let conversationIndex = 0;
|
|
let historyIndex = 0;
|
|
let forbiddenCalls = 0;
|
|
|
|
const messageService = {
|
|
async fetchMessagesWithoutUpdateToRead(options, conversation) {
|
|
historyCalls.push({ options, conversation });
|
|
return historyPages[historyIndex++];
|
|
},
|
|
fetchMessages() {
|
|
forbiddenCalls += 1;
|
|
},
|
|
updateMessageToRead() {
|
|
forbiddenCalls += 1;
|
|
},
|
|
};
|
|
const sdk = {
|
|
getConversationServiceV2() {
|
|
return {
|
|
async getConversationListByPagination(options) {
|
|
conversationCalls.push(options);
|
|
return conversationPages[conversationIndex++];
|
|
},
|
|
};
|
|
},
|
|
getMessageService() {
|
|
return messageService;
|
|
},
|
|
};
|
|
const pageWindow = {
|
|
location: {
|
|
href: `https://onetalk.alibaba.com/?activeAccountId=${targetAccountId}`,
|
|
},
|
|
IcbuIM: {
|
|
IMBaaSSDK: { default: sdk },
|
|
},
|
|
};
|
|
|
|
return {
|
|
pageWindow,
|
|
conversationCalls,
|
|
historyCalls,
|
|
messageService,
|
|
forbiddenCalls: () => forbiddenCalls,
|
|
};
|
|
}
|
|
|
|
test("accepts only finite non-empty OneTalk account identifiers", () => {
|
|
for (const value of ["account", 0, -1, Number.MAX_SAFE_INTEGER]) {
|
|
assert.equal(isAccountId(value), true);
|
|
}
|
|
|
|
for (const value of ["", Number.NaN, Number.POSITIVE_INFINITY, null, true]) {
|
|
assert.equal(isAccountId(value), false);
|
|
}
|
|
});
|
|
|
|
test("scans the current conversation and exhausts history through the no-read API", async () => {
|
|
const fixture = createPageWindow({
|
|
conversationPages: [
|
|
{
|
|
list: [{ ...targetConversation, accountId: "other" }],
|
|
hasMore: true,
|
|
nextCursor: "page-2",
|
|
},
|
|
{ list: [targetConversation], hasMore: false },
|
|
],
|
|
historyPages: [
|
|
{
|
|
list: [historyMessage(4, 400), historyMessage(3, 300)],
|
|
hasMore: true,
|
|
},
|
|
{
|
|
list: [historyMessage(3, 300), historyMessage(2, 200)],
|
|
hasMore: true,
|
|
},
|
|
{ list: [historyMessage(1, 100)], hasMore: false },
|
|
],
|
|
});
|
|
const sleeps = [];
|
|
const randomValues = [0, 1];
|
|
|
|
const result = await syncCurrentConversationHistory(fixture.pageWindow, {
|
|
now: () => 500,
|
|
random: () => randomValues.shift(),
|
|
sleep: async (milliseconds) => sleeps.push(milliseconds),
|
|
});
|
|
|
|
assert.deepEqual(result, { exit: "history_exhausted", pages: 3, uniqueMessages: 4 });
|
|
assert.deepEqual(fixture.conversationCalls, [
|
|
{ cursor: 0, count: 20 },
|
|
{ cursor: "page-2", count: 20 },
|
|
]);
|
|
assert.deepEqual(
|
|
fixture.historyCalls.map(({ options }) => options),
|
|
[
|
|
{
|
|
contactAccountId: targetAccountId,
|
|
contactAccountIdEncrypt: "encrypted-contact-account",
|
|
aliId: "seller@icbu",
|
|
aliIdEncrypt: "encrypted-seller-account",
|
|
searchMessageId: "",
|
|
timeSlide: { forward: false, timeStamp: 500, pageSize: 20 },
|
|
},
|
|
{
|
|
contactAccountId: targetAccountId,
|
|
contactAccountIdEncrypt: "encrypted-contact-account",
|
|
aliId: "seller@icbu",
|
|
aliIdEncrypt: "encrypted-seller-account",
|
|
searchMessageId: "",
|
|
timeSlide: { forward: false, timeStamp: 300, pageSize: 20 },
|
|
},
|
|
{
|
|
contactAccountId: targetAccountId,
|
|
contactAccountIdEncrypt: "encrypted-contact-account",
|
|
aliId: "seller@icbu",
|
|
aliIdEncrypt: "encrypted-seller-account",
|
|
searchMessageId: "",
|
|
timeSlide: { forward: false, timeStamp: 200, pageSize: 20 },
|
|
},
|
|
],
|
|
);
|
|
assert.equal(fixture.historyCalls[0].conversation, targetConversation);
|
|
assert.deepEqual(sleeps, [1_000, 3_000]);
|
|
assert.equal(fixture.forbiddenCalls(), 0);
|
|
});
|
|
|
|
test("the installed global entry has no dependency override and returns completion", async () => {
|
|
const fixture = createPageWindow({
|
|
conversationPages: [{ list: [targetConversation], hasMore: false }],
|
|
historyPages: [{ list: [historyMessage("m1", 100)], hasMore: false }],
|
|
});
|
|
|
|
installCurrentConversationHistorySync(fixture.pageWindow);
|
|
const entry = fixture.pageWindow.__tradeMessageCenterOneTalk.syncCurrentConversationHistory;
|
|
|
|
assert.equal(entry.length, 0);
|
|
assert.deepEqual(await entry(), { exit: "history_exhausted", pages: 1, uniqueMessages: 1 });
|
|
});
|
|
|
|
test("skips unrelated conversation shapes and validates only the active account match", async () => {
|
|
const fixture = createPageWindow({
|
|
conversationPages: [
|
|
{
|
|
list: [
|
|
{ cid: "group-conversation" },
|
|
{ accountId: "system-conversation" },
|
|
targetConversation,
|
|
],
|
|
hasMore: false,
|
|
},
|
|
],
|
|
historyPages: [{ list: [historyMessage("m1", 100)], hasMore: false }],
|
|
});
|
|
|
|
assert.deepEqual(await syncCurrentConversationHistory(fixture.pageWindow, { now: () => 500 }), {
|
|
exit: "history_exhausted",
|
|
pages: 1,
|
|
uniqueMessages: 1,
|
|
});
|
|
assert.equal(fixture.historyCalls[0].conversation, targetConversation);
|
|
});
|
|
|
|
test("converts SDK request failures to a stable secret-free error", async () => {
|
|
const fixture = createPageWindow({
|
|
conversationPages: [{ list: [targetConversation], hasMore: false }],
|
|
historyPages: [],
|
|
});
|
|
fixture.messageService.fetchMessagesWithoutUpdateToRead = async () => {
|
|
throw new Error("chatToken=PAGE-SECRET&contactAccountIdEncrypt=BUYER-SECRET");
|
|
};
|
|
|
|
const error = await syncCurrentConversationHistory(fixture.pageWindow).catch(
|
|
(reason) => reason,
|
|
);
|
|
assert.equal(error.message, "onetalk_history_message_request_failed");
|
|
assert.equal(error.message.includes("SECRET"), false);
|
|
});
|
|
|
|
test("reuses the exact in-flight promise for the same page", async () => {
|
|
let resolveHistory;
|
|
const historyResponse = new Promise((resolve) => {
|
|
resolveHistory = resolve;
|
|
});
|
|
const fixture = createPageWindow({
|
|
conversationPages: [{ list: [targetConversation], hasMore: false }],
|
|
historyPages: [historyResponse],
|
|
});
|
|
const dependencies = { now: () => 500, sleep: async () => {}, random: () => 0 };
|
|
|
|
const first = syncCurrentConversationHistory(fixture.pageWindow, dependencies);
|
|
const second = syncCurrentConversationHistory(fixture.pageWindow, dependencies);
|
|
assert.equal(first, second);
|
|
|
|
resolveHistory({ list: [historyMessage("m1", 100)], hasMore: false });
|
|
await first;
|
|
assert.equal(fixture.historyCalls.length, 1);
|
|
});
|
|
|
|
test("fails closed when conversation pagination does not advance", async () => {
|
|
const fixture = createPageWindow({
|
|
conversationPages: [
|
|
{
|
|
list: [{ ...targetConversation, accountId: "other" }],
|
|
hasMore: true,
|
|
nextCursor: "same",
|
|
},
|
|
{
|
|
list: [{ ...targetConversation, accountId: "other" }],
|
|
hasMore: true,
|
|
nextCursor: "same",
|
|
},
|
|
],
|
|
historyPages: [],
|
|
});
|
|
|
|
await assert.rejects(
|
|
syncCurrentConversationHistory(fixture.pageWindow),
|
|
/onetalk_history_conversation_cursor_stalled/,
|
|
);
|
|
});
|
|
|
|
test("fails closed when history pages cannot make safe progress", async (t) => {
|
|
const cases = [
|
|
{ name: "empty page", page: { list: [], hasMore: false }, error: "invalid_message_page" },
|
|
{
|
|
name: "missing finite sendTime",
|
|
page: { list: [{ messageId: 1 }], hasMore: false },
|
|
error: "invalid_message_page",
|
|
},
|
|
{
|
|
name: "time cursor stalls",
|
|
page: { list: [historyMessage("m1", 500)], hasMore: false },
|
|
error: "message_cursor_stalled",
|
|
},
|
|
];
|
|
|
|
for (const scenario of cases) {
|
|
await t.test(scenario.name, async () => {
|
|
const fixture = createPageWindow({
|
|
conversationPages: [{ list: [targetConversation], hasMore: false }],
|
|
historyPages: [scenario.page],
|
|
});
|
|
await assert.rejects(
|
|
syncCurrentConversationHistory(fixture.pageWindow, { now: () => 500 }),
|
|
new RegExp(`onetalk_history_${scenario.error}`),
|
|
);
|
|
});
|
|
}
|
|
});
|
|
|
|
test("fails closed when a page contains no new message IDs", async () => {
|
|
const fixture = createPageWindow({
|
|
conversationPages: [{ list: [targetConversation], hasMore: false }],
|
|
historyPages: [
|
|
{ list: [historyMessage(1, 300)], hasMore: true },
|
|
{ list: [historyMessage("1", 200)], hasMore: false },
|
|
],
|
|
});
|
|
|
|
await assert.rejects(
|
|
syncCurrentConversationHistory(fixture.pageWindow, {
|
|
now: () => 500,
|
|
random: () => 0,
|
|
sleep: async () => {},
|
|
}),
|
|
/onetalk_history_message_cursor_stalled/,
|
|
);
|
|
});
|
|
|
|
test("stops when the active account changes during a history request", async () => {
|
|
const fixture = createPageWindow({
|
|
conversationPages: [{ list: [targetConversation], hasMore: false }],
|
|
historyPages: [{ list: [historyMessage("m1", 100)], hasMore: false }],
|
|
});
|
|
const originalFetch = fixture.messageService.fetchMessagesWithoutUpdateToRead;
|
|
fixture.messageService.fetchMessagesWithoutUpdateToRead = async (...args) => {
|
|
const response = await originalFetch(...args);
|
|
fixture.pageWindow.location.href =
|
|
"https://onetalk.alibaba.com/?activeAccountId=different-account";
|
|
return response;
|
|
};
|
|
|
|
await assert.rejects(
|
|
syncCurrentConversationHistory(fixture.pageWindow),
|
|
/onetalk_history_active_account_changed/,
|
|
);
|
|
});
|
|
|
|
test("fails closed when the required page SDK is unavailable", async () => {
|
|
let topLevelGetterCalls = 0;
|
|
await assert.rejects(
|
|
syncCurrentConversationHistory({
|
|
location: {
|
|
href: `https://onetalk.alibaba.com/?activeAccountId=${targetAccountId}`,
|
|
},
|
|
getConversationServiceV2() {
|
|
topLevelGetterCalls += 1;
|
|
},
|
|
getMessageService() {
|
|
topLevelGetterCalls += 1;
|
|
},
|
|
}),
|
|
/onetalk_history_sdk_unavailable/,
|
|
);
|
|
assert.equal(topLevelGetterCalls, 0);
|
|
});
|
|
|
|
test("fails closed when the matched conversation lacks required identity fields", async () => {
|
|
const fixture = createPageWindow({
|
|
conversationPages: [
|
|
{
|
|
list: [{ ...targetConversation, cid: undefined }],
|
|
hasMore: false,
|
|
},
|
|
],
|
|
historyPages: [],
|
|
});
|
|
|
|
await assert.rejects(
|
|
syncCurrentConversationHistory(fixture.pageWindow),
|
|
/onetalk_history_invalid_conversation_page/,
|
|
);
|
|
assert.equal(fixture.historyCalls.length, 0);
|
|
});
|