Files
trade-message-center/apps/chrome-extension/test/onetalk-current-conversation-history.test.js
T

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);
});