mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
751 lines
26 KiB
JavaScript
751 lines
26 KiB
JavaScript
// 验证 OneTalk 页面桥的边界与双向传输
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
createOneTalkBuyerFactFingerprint,
|
|
createOneTalkRenderedCardContentFingerprint,
|
|
} from "@trade-message-center/onetalk-contract";
|
|
import {
|
|
createOneTalkPageCommandMessage,
|
|
createOneTalkPageCommandResultMessage,
|
|
createOneTalkPageBindingStatusMessage,
|
|
createOneTalkPageConnectionStatusMessage,
|
|
createOneTalkPageObservedMessage,
|
|
createOneTalkPageProfileObservedMessage,
|
|
createOneTalkPageBuyerFactsObservedMessage,
|
|
createOneTalkPageRenderedCardObservedMessage,
|
|
decodeOneTalkPageConversationDiscoveryResult,
|
|
decodeOneTalkPageMessage,
|
|
isOneTalkMainToIsolatedMessage,
|
|
ONE_TALK_PAGE_BRIDGE_SOURCE,
|
|
ONE_TALK_PAGE_BRIDGE_VERSION,
|
|
ONE_TALK_PAGE_PORT_NAME,
|
|
} from "../src/onetalk/page-bridge/model.ts";
|
|
import { readChannelAccountId } from "../src/onetalk/main-page/page-context.ts";
|
|
import { installOneTalkIsolatedPageBridge } from "../src/onetalk/page-bridge/isolated.ts";
|
|
import { installOneTalkMainPageBridge } from "../src/onetalk/page-bridge/main.ts";
|
|
import { installOneTalkMessageObserver } from "../src/onetalk/main-page/message-observer/entry.ts";
|
|
|
|
const pageOrigin = "https://onetalk.alibaba.com";
|
|
|
|
const observedMessage = {
|
|
messageType: "new",
|
|
upstreamType: 1,
|
|
conversationId: "conversation-1",
|
|
messageId: "message-1",
|
|
sentAtMs: 1_700_000_000_000,
|
|
content: { version: 1, kind: "text", text: "hello" },
|
|
senderId: "buyer@icbu",
|
|
participantIds: ["buyer@icbu", "seller@icbu"],
|
|
direction: "received",
|
|
readStatus: 0,
|
|
messageStatus: 1,
|
|
unreadCount: 0,
|
|
};
|
|
|
|
const profile = {
|
|
conversationId: "conversation-1",
|
|
aliId: "2208314000798",
|
|
accountId: "243340382",
|
|
loginId: "hzhago",
|
|
name: "Heena Liu",
|
|
companyName: "Hago",
|
|
countryCode: "CN",
|
|
currentTimeZone: -9,
|
|
serviceType: "cgs",
|
|
avatarUrl: "https://cdn.example.test/avatar/customer-1.jpg",
|
|
observedAtMs: 1_700_000_000_000,
|
|
profileFingerprint: "v1-profile",
|
|
observationStatus: "confirmed",
|
|
};
|
|
|
|
const extendedBuyerFact = {
|
|
conversationId: "conversation-1",
|
|
buyerTags: ["high-potential"],
|
|
buyerFeatures: ["repeat-buyer"],
|
|
tags: {
|
|
state: "confirmed",
|
|
errorCode: null,
|
|
attemptedAtMs: 1_700_000_000_000,
|
|
confirmedAtMs: 1_700_000_000_000,
|
|
},
|
|
features: {
|
|
state: "confirmed",
|
|
errorCode: null,
|
|
attemptedAtMs: 1_700_000_000_000,
|
|
confirmedAtMs: 1_700_000_000_000,
|
|
},
|
|
email: "buyer@example.test",
|
|
registrationDate: "2025-03-12",
|
|
companyWebsite: "https://example.test/",
|
|
contactDetails: {
|
|
state: "confirmed",
|
|
errorCode: null,
|
|
attemptedAtMs: 1_700_000_000_000,
|
|
confirmedAtMs: 1_700_000_000_000,
|
|
},
|
|
observedAtMs: 1_700_000_000_000,
|
|
factFingerprint: createOneTalkBuyerFactFingerprint(["high-potential"], ["repeat-buyer"], {
|
|
email: "buyer@example.test",
|
|
registrationDate: "2025-03-12",
|
|
companyWebsite: "https://example.test/",
|
|
}),
|
|
};
|
|
|
|
class FakeWebSocket extends EventTarget {
|
|
static OPEN = 1;
|
|
|
|
constructor(url) {
|
|
super();
|
|
this.url = String(url);
|
|
}
|
|
|
|
receive(data) {
|
|
this.dispatchEvent(new MessageEvent("message", { data }));
|
|
}
|
|
}
|
|
|
|
class FakePageWindow {
|
|
constructor() {
|
|
this.location = {
|
|
href: `${pageOrigin}/?activeAccountId=account-1`,
|
|
origin: pageOrigin,
|
|
};
|
|
this.currentUserAccountId = "login-account-1";
|
|
this.__conversationListFullData__ = [
|
|
{ owner: { accountId: "login-account-1", aliId: "seller" } },
|
|
];
|
|
this.listeners = [];
|
|
this.posted = [];
|
|
this.throwOnPost = false;
|
|
this.logs = [];
|
|
this.console = {
|
|
log: (...args) => this.logs.push(args),
|
|
};
|
|
this.WebSocket = FakeWebSocket;
|
|
}
|
|
|
|
addEventListener(type, listener) {
|
|
if (type === "message" || type === "pagehide") this.listeners.push({ type, listener });
|
|
}
|
|
|
|
postMessage(message, targetOrigin) {
|
|
if (this.throwOnPost) throw new Error("post failed");
|
|
this.posted.push({ message, targetOrigin });
|
|
}
|
|
|
|
dispatchMessage(data, { source = this, origin = pageOrigin } = {}) {
|
|
for (const entry of this.listeners) {
|
|
if (entry.type === "message") entry.listener({ data, source, origin });
|
|
}
|
|
}
|
|
|
|
dispatchPageHide() {
|
|
for (const entry of this.listeners) {
|
|
if (entry.type === "pagehide") entry.listener();
|
|
}
|
|
}
|
|
}
|
|
|
|
class FakePort {
|
|
constructor(name = ONE_TALK_PAGE_PORT_NAME) {
|
|
this.name = name;
|
|
this.posted = [];
|
|
this.messageListeners = [];
|
|
this.disconnectListeners = [];
|
|
}
|
|
|
|
postMessage(message) {
|
|
this.posted.push(message);
|
|
}
|
|
|
|
onMessage = {
|
|
addListener: (listener) => this.messageListeners.push(listener),
|
|
};
|
|
|
|
onDisconnect = {
|
|
addListener: (listener) => this.disconnectListeners.push(listener),
|
|
};
|
|
|
|
dispatchMessage(message) {
|
|
for (const listener of this.messageListeners) listener(message);
|
|
}
|
|
|
|
disconnect() {
|
|
for (const listener of this.disconnectListeners) listener();
|
|
}
|
|
}
|
|
|
|
const lastPostedMessage = (pageWindow) => {
|
|
return pageWindow.posted.at(-1)?.message;
|
|
};
|
|
|
|
test("decodes one versioned JSON envelope and rejects malformed shapes", () => {
|
|
const observed = createOneTalkPageObservedMessage([observedMessage]);
|
|
assert.deepEqual(decodeOneTalkPageMessage(observed), observed);
|
|
const invalidObservationDiagnostic = createOneTalkPageObservedMessage([], undefined, {
|
|
unsupportedSkippedCount: 0,
|
|
invalidObservationCount: 1,
|
|
anomalies: [],
|
|
});
|
|
assert.deepEqual(
|
|
decodeOneTalkPageMessage(invalidObservationDiagnostic),
|
|
invalidObservationDiagnostic,
|
|
);
|
|
for (const code of [
|
|
"card_invalid_base64",
|
|
"card_invalid_utf8",
|
|
"card_invalid_json",
|
|
"card_payload_too_large",
|
|
"card_invalid_schema",
|
|
]) {
|
|
const cardDiagnostic = createOneTalkPageObservedMessage([], undefined, {
|
|
unsupportedSkippedCount: 0,
|
|
invalidObservationCount: 0,
|
|
anomalies: [{ code, mediaKind: "card", count: 1 }],
|
|
});
|
|
assert.deepEqual(decodeOneTalkPageMessage(cardDiagnostic), cardDiagnostic);
|
|
}
|
|
assert.equal(
|
|
decodeOneTalkPageMessage({
|
|
...observed,
|
|
source: "other-source",
|
|
}),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
decodeOneTalkPageMessage({
|
|
...observed,
|
|
version: ONE_TALK_PAGE_BRIDGE_VERSION + 1,
|
|
}),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
decodeOneTalkPageMessage({
|
|
...observed,
|
|
type: "onetalk.page.unknown",
|
|
}),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
decodeOneTalkPageMessage({
|
|
...createOneTalkPageCommandMessage("request-1", { action: "send" }),
|
|
command: { action: undefined },
|
|
}),
|
|
null,
|
|
);
|
|
|
|
const progress = {
|
|
conversationId: "conversation-1",
|
|
latestMessageAtMs: 1_700_000_000_000,
|
|
page: 1,
|
|
mode: "incremental",
|
|
anchorMessageId: "anchor-1",
|
|
nextTimeStamp: 1_699_999_999_000,
|
|
historyComplete: false,
|
|
anchorFound: false,
|
|
};
|
|
const progressMessage = createOneTalkPageObservedMessage([], progress);
|
|
assert.deepEqual(decodeOneTalkPageMessage(progressMessage), progressMessage);
|
|
assert.equal(
|
|
decodeOneTalkPageMessage({
|
|
...progressMessage,
|
|
historyProgress: { ...progress, page: 0 },
|
|
}),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
decodeOneTalkPageMessage({
|
|
...progressMessage,
|
|
historyProgress: { ...progress, latestMessageAtMs: "1700000000000" },
|
|
}),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
decodeOneTalkPageMessage({
|
|
...progressMessage,
|
|
historyProgress: { ...progress, rawSdk: { chatToken: "secret" } },
|
|
}),
|
|
null,
|
|
);
|
|
|
|
for (const batch of [
|
|
[{ ...observedMessage, text: "legacy text" }],
|
|
[{ ...observedMessage, contentType: 1 }],
|
|
[
|
|
{
|
|
...observedMessage,
|
|
content: {
|
|
...observedMessage.content,
|
|
custom: { data: "raw-content", chatToken: "secret" },
|
|
},
|
|
},
|
|
],
|
|
[{ ...observedMessage, rawSdk: { chatToken: "secret" } }],
|
|
]) {
|
|
assert.equal(decodeOneTalkPageMessage({ ...observed, batch }), null);
|
|
}
|
|
assert.equal(
|
|
decodeOneTalkPageMessage({
|
|
...observed,
|
|
diagnostics: {
|
|
unsupportedSkippedCount: 0,
|
|
invalidObservationCount: 0,
|
|
anomalies: [
|
|
{ code: "media_invalid_json", mediaKind: "image", count: 1 },
|
|
{ code: "media_invalid_json", mediaKind: "image", count: 1 },
|
|
],
|
|
},
|
|
}),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
decodeOneTalkPageMessage({
|
|
...observed,
|
|
diagnostics: { unsupportedSkippedCount: 0, anomalies: [] },
|
|
}),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test("accepts only generic rendered-card observations and rejects forged fields", () => {
|
|
const content = {
|
|
version: 1,
|
|
title: "Order",
|
|
image: "https://img.alicdn.com/order.jpg",
|
|
productCount: { display: "5 products", value: 5 },
|
|
status: "Paid",
|
|
};
|
|
const observation = {
|
|
conversationId: "conversation-1",
|
|
messageId: "message-1",
|
|
content,
|
|
contentFingerprint: createOneTalkRenderedCardContentFingerprint(content),
|
|
observedAtMs: 1_700_000_000_000,
|
|
};
|
|
const message = createOneTalkPageRenderedCardObservedMessage("account-1", [observation]);
|
|
assert.deepEqual(decodeOneTalkPageMessage(message), message);
|
|
|
|
const legacyContent = {
|
|
version: 1,
|
|
kind: "rendered_order",
|
|
title: "Order",
|
|
products: [],
|
|
productCount: 0,
|
|
status: { code: null, text: "Paid" },
|
|
payment: { totalDisplay: "$1", discountDisplay: null },
|
|
delivery: { shippingAddress: "Address", methodLabel: null, dateLabel: null },
|
|
action: { label: null, status: null },
|
|
};
|
|
const legacyObservation = {
|
|
...observation,
|
|
content: legacyContent,
|
|
contentFingerprint: createOneTalkRenderedCardContentFingerprint(legacyContent),
|
|
};
|
|
assert.equal(decodeOneTalkPageMessage({ ...message, observations: [legacyObservation] }), null);
|
|
assert.equal(
|
|
decodeOneTalkPageMessage({
|
|
...message,
|
|
observations: [{ ...observation, extra: "unexpected" }],
|
|
}),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
decodeOneTalkPageMessage({
|
|
...message,
|
|
observations: [{ ...observation, contentFingerprint: "forged" }],
|
|
}),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
decodeOneTalkPageMessage({
|
|
...message,
|
|
observations: [{ ...observation, conversationId: "" }],
|
|
}),
|
|
null,
|
|
);
|
|
assert.equal(decodeOneTalkPageMessage({ ...message, baseEvidence: [] }), null);
|
|
});
|
|
|
|
test("rejects legacy typed rendered products at the observed-frame boundary", () => {
|
|
const content = {
|
|
version: 1,
|
|
kind: "rendered_product",
|
|
product: {
|
|
imageUrl: "https://img.alicdn.com/product/widget.jpg",
|
|
title: "Widget",
|
|
sourceUrl: "https://chinese.alibaba.com/product-detail/Widget-123456789.html",
|
|
productId: "123456789",
|
|
},
|
|
priceDisplay: "$9.99",
|
|
minimumOrder: { value: "10", unit: "pieces" },
|
|
serviceBadges: ["Trade Assurance", "Fast dispatch"],
|
|
};
|
|
const observation = {
|
|
conversationId: "conversation-1",
|
|
messageId: "product-message-1",
|
|
content,
|
|
contentFingerprint: createOneTalkRenderedCardContentFingerprint(content),
|
|
observedAtMs: 1_700_000_000_000,
|
|
};
|
|
const message = {
|
|
source: ONE_TALK_PAGE_BRIDGE_SOURCE,
|
|
version: ONE_TALK_PAGE_BRIDGE_VERSION,
|
|
type: "onetalk.page.rendered-card-observed",
|
|
channelAccountId: "account-1",
|
|
observations: [observation],
|
|
};
|
|
|
|
assert.equal(decodeOneTalkPageMessage(message), null);
|
|
});
|
|
|
|
test("deep-clones extended buyer facts at the page bridge boundary", () => {
|
|
const created = createOneTalkPageBuyerFactsObservedMessage(
|
|
[extendedBuyerFact],
|
|
"login-account-1",
|
|
);
|
|
assert.notEqual(created.facts[0], extendedBuyerFact);
|
|
assert.notEqual(created.facts[0].tags, extendedBuyerFact.tags);
|
|
assert.notEqual(created.facts[0].contactDetails, extendedBuyerFact.contactDetails);
|
|
|
|
const decoded = decodeOneTalkPageMessage(created);
|
|
assert.ok(decoded && decoded.type === "onetalk.page.buyer-facts-observed");
|
|
if (!decoded || decoded.type !== "onetalk.page.buyer-facts-observed") return;
|
|
|
|
created.facts[0].buyerTags.push("mutated");
|
|
created.facts[0].contactDetails.state = "failed";
|
|
assert.deepEqual(extendedBuyerFact.buyerTags, ["high-potential"]);
|
|
assert.equal(extendedBuyerFact.contactDetails.state, "confirmed");
|
|
|
|
assert.notEqual(decoded.facts[0], created.facts[0]);
|
|
assert.notEqual(decoded.facts[0].contactDetails, created.facts[0].contactDetails);
|
|
decoded.facts[0].contactDetails.confirmedAtMs = null;
|
|
assert.equal(created.facts[0].contactDetails.confirmedAtMs, 1_700_000_000_000);
|
|
});
|
|
|
|
test("accepts only the direct discovery whitelist before Bright handoff", () => {
|
|
const result = {
|
|
status: "completed",
|
|
entries: [
|
|
{
|
|
conversationId: "conversation-1",
|
|
lastContactTimeLong: 1_700_000_000_000,
|
|
messagePreview: "safe preview",
|
|
},
|
|
],
|
|
};
|
|
assert.deepEqual(decodeOneTalkPageConversationDiscoveryResult(result), result);
|
|
for (const entries of [
|
|
[{ ...result.entries[0], chatToken: "secret" }],
|
|
[{ ...result.entries[0], messagePreview: "\u0000invalid" }],
|
|
[result.entries[0], result.entries[0]],
|
|
]) {
|
|
assert.equal(
|
|
decodeOneTalkPageConversationDiscoveryResult({ status: "completed", entries }),
|
|
null,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("rejects observed messages with incomplete direct identity", () => {
|
|
for (const overrides of [
|
|
{ participantIds: [] },
|
|
{ participantIds: ["buyer@icbu"] },
|
|
{ participantIds: ["buyer@icbu", "buyer@icbu"] },
|
|
{ senderId: "outside@icbu" },
|
|
]) {
|
|
const observed = createOneTalkPageObservedMessage([{ ...observedMessage, ...overrides }]);
|
|
assert.equal(decodeOneTalkPageMessage(observed), null);
|
|
}
|
|
});
|
|
|
|
test("requires the exact MAIN raw-type proof for every observed message", () => {
|
|
const observed = createOneTalkPageObservedMessage([observedMessage]);
|
|
for (const upstreamType of [undefined, 0, 2, "1"]) {
|
|
const forged = { ...observedMessage, upstreamType };
|
|
if (upstreamType === undefined) delete forged.upstreamType;
|
|
assert.equal(decodeOneTalkPageMessage({ ...observed, batch: [forged] }), null);
|
|
}
|
|
});
|
|
|
|
test("forwards only valid current-window MAIN messages once to the named Port", () => {
|
|
const pageWindow = new FakePageWindow();
|
|
const port = new FakePort();
|
|
installOneTalkIsolatedPageBridge(pageWindow, port);
|
|
|
|
const observed = createOneTalkPageObservedMessage([observedMessage]);
|
|
pageWindow.dispatchMessage(observed);
|
|
pageWindow.dispatchMessage(observed, { source: {} });
|
|
pageWindow.dispatchMessage(observed, { origin: "https://evil.example" });
|
|
pageWindow.dispatchMessage({
|
|
...observed,
|
|
source: "wrong-source",
|
|
});
|
|
|
|
assert.deepEqual(port.posted, [observed]);
|
|
});
|
|
|
|
test("forwards exact Service Worker display statuses to MAIN without a command result", () => {
|
|
const pageWindow = new FakePageWindow();
|
|
const port = new FakePort();
|
|
const statuses = [];
|
|
const bindingStatuses = [];
|
|
installOneTalkIsolatedPageBridge(pageWindow, port);
|
|
installOneTalkMainPageBridge(
|
|
pageWindow,
|
|
undefined,
|
|
undefined,
|
|
(disconnected) => {
|
|
statuses.push(disconnected);
|
|
},
|
|
(unbound) => bindingStatuses.push(unbound),
|
|
);
|
|
pageWindow.posted.length = 0;
|
|
|
|
const status = createOneTalkPageConnectionStatusMessage(true);
|
|
assert.equal(isOneTalkMainToIsolatedMessage(status), false);
|
|
port.dispatchMessage(status);
|
|
assert.deepEqual(lastPostedMessage(pageWindow), status);
|
|
pageWindow.dispatchMessage(lastPostedMessage(pageWindow));
|
|
assert.deepEqual(statuses, [true]);
|
|
assert.equal(
|
|
pageWindow.posted.some((entry) => entry.message.type === "onetalk.page.command-result"),
|
|
false,
|
|
);
|
|
|
|
for (const malformed of [
|
|
{ ...status, disconnected: "true" },
|
|
{ ...status, detail: "socket closed" },
|
|
{ ...status, source: "other-source" },
|
|
{ ...status, version: ONE_TALK_PAGE_BRIDGE_VERSION + 1 },
|
|
{ source: status.source, version: status.version, type: status.type },
|
|
]) {
|
|
port.dispatchMessage(malformed);
|
|
}
|
|
pageWindow.dispatchMessage(status, { origin: "https://evil.example" });
|
|
assert.deepEqual(statuses, [true]);
|
|
|
|
const bindingStatus = createOneTalkPageBindingStatusMessage(true);
|
|
assert.equal(isOneTalkMainToIsolatedMessage(bindingStatus), false);
|
|
port.dispatchMessage(bindingStatus);
|
|
assert.deepEqual(lastPostedMessage(pageWindow), bindingStatus);
|
|
pageWindow.dispatchMessage(lastPostedMessage(pageWindow));
|
|
assert.deepEqual(bindingStatuses, [true]);
|
|
assert.equal(
|
|
pageWindow.posted.some((entry) => entry.message.type === "onetalk.page.command-result"),
|
|
false,
|
|
);
|
|
|
|
for (const malformed of [
|
|
{ ...bindingStatus, unbound: "true" },
|
|
{ ...bindingStatus, binding: "secret" },
|
|
{ ...bindingStatus, source: "other-source" },
|
|
{ ...bindingStatus, version: ONE_TALK_PAGE_BRIDGE_VERSION + 1 },
|
|
{ source: bindingStatus.source, version: bindingStatus.version, type: bindingStatus.type },
|
|
]) {
|
|
port.dispatchMessage(malformed);
|
|
}
|
|
pageWindow.dispatchMessage(bindingStatus, { origin: "https://evil.example" });
|
|
assert.deepEqual(bindingStatuses, [true]);
|
|
});
|
|
|
|
test("keeps the profile envelope identity explicit and rejects sensitive or extra fields", () => {
|
|
const observed = createOneTalkPageProfileObservedMessage([profile], "login-account-1");
|
|
assert.deepEqual(decodeOneTalkPageMessage(observed), observed);
|
|
assert.equal(decodeOneTalkPageMessage({ ...observed, channelAccountId: "" }), null);
|
|
assert.equal(decodeOneTalkPageMessage({ ...observed, chatToken: "secret-chat-token" }), null);
|
|
assert.equal(
|
|
decodeOneTalkPageMessage({
|
|
...observed,
|
|
profiles: [{ ...profile, rawRow: { chatToken: "secret-raw-row" } }],
|
|
}),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test("strictly decodes plain canonical envelopes while raw adapters remain elsewhere", () => {
|
|
const command = createOneTalkPageCommandMessage("request-plain-record", { action: "send" });
|
|
const nullPrototype = Object.assign(Object.create(null), command);
|
|
const customPrototype = Object.assign(Object.create({}), command);
|
|
const nestedCustomPrototype = {
|
|
...command,
|
|
command: Object.assign(Object.create({}), command.command),
|
|
};
|
|
|
|
assert.deepEqual(decodeOneTalkPageMessage(nullPrototype), command);
|
|
assert.equal(decodeOneTalkPageMessage(customPrototype), null);
|
|
assert.equal(decodeOneTalkPageMessage(nestedCustomPrototype), null);
|
|
});
|
|
|
|
test("publishes a page registration to the current origin", () => {
|
|
const pageWindow = new FakePageWindow();
|
|
installOneTalkMainPageBridge(pageWindow);
|
|
|
|
assert.equal(pageWindow.posted.length, 1);
|
|
assert.deepEqual(pageWindow.posted[0], {
|
|
message: {
|
|
source: ONE_TALK_PAGE_BRIDGE_SOURCE,
|
|
version: ONE_TALK_PAGE_BRIDGE_VERSION,
|
|
type: "onetalk.page.hello",
|
|
channelAccountId: "login-account-1",
|
|
},
|
|
targetOrigin: pageOrigin,
|
|
});
|
|
});
|
|
|
|
test("reads the logged-in account from the OneTalk UserUtil fallback", () => {
|
|
const pageWindow = new FakePageWindow();
|
|
pageWindow.currentUserAccountId = undefined;
|
|
pageWindow.IcbuIM = {
|
|
UserUtil: { currentUser: { accountId: 286995452 } },
|
|
};
|
|
|
|
assert.equal(readChannelAccountId(pageWindow), "286995452");
|
|
});
|
|
|
|
test("keeps raw custom-prototype UserUtil adapters readable", () => {
|
|
const pageWindow = new FakePageWindow();
|
|
pageWindow.currentUserAccountId = undefined;
|
|
pageWindow.IcbuIM = Object.assign(Object.create({}), {
|
|
UserUtil: Object.assign(Object.create({}), {
|
|
currentUser: Object.assign(Object.create({}), { accountId: "host-account-1" }),
|
|
}),
|
|
});
|
|
|
|
assert.equal(readChannelAccountId(pageWindow), "host-account-1");
|
|
});
|
|
|
|
test("never falls back to the URL active contact account", () => {
|
|
const pageWindow = new FakePageWindow();
|
|
pageWindow.currentUserAccountId = undefined;
|
|
|
|
assert.equal(readChannelAccountId(pageWindow), null);
|
|
});
|
|
|
|
test("retries page registration when the account becomes available, then stops", async () => {
|
|
const pageWindow = new FakePageWindow();
|
|
pageWindow.location.href = `${pageOrigin}/`;
|
|
pageWindow.currentUserAccountId = null;
|
|
installOneTalkMainPageBridge(pageWindow, undefined, { retryDelayMs: 1, maxAttempts: 4 });
|
|
assert.equal(pageWindow.posted.length, 0);
|
|
pageWindow.currentUserAccountId = "login-late";
|
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
assert.equal(pageWindow.posted.length, 1);
|
|
assert.equal(pageWindow.posted[0].message.channelAccountId, "login-late");
|
|
pageWindow.dispatchPageHide();
|
|
pageWindow.location.href = `${pageOrigin}/?activeAccountId=another-account`;
|
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
assert.equal(pageWindow.posted.length, 1);
|
|
});
|
|
|
|
test("routes a Port command through MAIN and returns its request correlation", async () => {
|
|
const pageWindow = new FakePageWindow();
|
|
const port = new FakePort();
|
|
const commands = [];
|
|
installOneTalkIsolatedPageBridge(pageWindow, port);
|
|
installOneTalkMainPageBridge(pageWindow, (message) => {
|
|
commands.push(message);
|
|
return { accepted: true };
|
|
});
|
|
pageWindow.posted.length = 0;
|
|
|
|
const command = createOneTalkPageCommandMessage("request-1", { action: "send" });
|
|
port.dispatchMessage(command);
|
|
pageWindow.dispatchMessage(lastPostedMessage(pageWindow));
|
|
await Promise.resolve();
|
|
|
|
const result = lastPostedMessage(pageWindow);
|
|
assert.deepEqual(commands, [command]);
|
|
assert.equal(result.type, "onetalk.page.command-result");
|
|
assert.equal(result.requestId, "request-1");
|
|
|
|
pageWindow.dispatchMessage(result);
|
|
assert.deepEqual(port.posted, [result]);
|
|
});
|
|
|
|
test("does not publish a command result after pagehide cleanup", async () => {
|
|
const pageWindow = new FakePageWindow();
|
|
let release;
|
|
const pending = new Promise((resolve) => {
|
|
release = resolve;
|
|
});
|
|
installOneTalkMainPageBridge(pageWindow, () => pending);
|
|
pageWindow.posted.length = 0;
|
|
const command = createOneTalkPageCommandMessage("request-cleanup", { action: "send" });
|
|
pageWindow.dispatchMessage(command);
|
|
pageWindow.dispatchPageHide();
|
|
release({ accepted: true });
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
assert.deepEqual(pageWindow.posted, []);
|
|
});
|
|
|
|
test("drops messages after Port disconnect without creating a local queue", () => {
|
|
const pageWindow = new FakePageWindow();
|
|
const port = new FakePort();
|
|
installOneTalkIsolatedPageBridge(pageWindow, port);
|
|
port.disconnect();
|
|
|
|
pageWindow.dispatchMessage(createOneTalkPageObservedMessage([observedMessage]));
|
|
port.dispatchMessage(createOneTalkPageCommandMessage("request-1", { action: "send" }));
|
|
|
|
assert.deepEqual(port.posted, []);
|
|
assert.deepEqual(pageWindow.posted, []);
|
|
});
|
|
|
|
test("keeps MAIN observer behavior when the injected sink throws", () => {
|
|
const pageWindow = new FakePageWindow();
|
|
const batches = [];
|
|
installOneTalkMessageObserver(pageWindow, (batch) => {
|
|
batches.push(batch);
|
|
throw new Error("sink failed");
|
|
});
|
|
|
|
const socket = new pageWindow.WebSocket("wss://wss-icbu.dingtalk.com/");
|
|
socket.receive(
|
|
JSON.stringify({
|
|
code: 200,
|
|
body: [
|
|
{
|
|
singleChatUserConversation: {
|
|
lastMessage: {
|
|
message: {
|
|
type: 1,
|
|
messageId: "message-1",
|
|
cid: "buyer-seller#tenant@icbu",
|
|
createAt: 1_700_000_000_000,
|
|
content: { text: { content: "hello" }, contentType: 1 },
|
|
sender: { uid: "buyer@icbu" },
|
|
unreadCount: 0,
|
|
},
|
|
readStatus: 0,
|
|
msgStatus: 1,
|
|
},
|
|
singleChatConversation: {
|
|
pairFirst: "buyer@icbu",
|
|
pairSecond: "seller@icbu",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
|
|
assert.equal(batches.length, 1);
|
|
assert.equal(batches[0].messages[0].messageId, "message-1");
|
|
assert.deepEqual(batches[0].diagnostics, {
|
|
unsupportedSkippedCount: 0,
|
|
invalidObservationCount: 0,
|
|
anomalies: [],
|
|
});
|
|
assert.equal(pageWindow.logs.length, 0);
|
|
});
|
|
|
|
test("uses the fixed bridge source in command results", () => {
|
|
const result = createOneTalkPageCommandResultMessage("request-2", { accepted: false });
|
|
assert.equal(result.source, ONE_TALK_PAGE_BRIDGE_SOURCE);
|
|
assert.equal(result.version, ONE_TALK_PAGE_BRIDGE_VERSION);
|
|
});
|