Files
trade-message-center/apps/chrome-extension/test/onetalk-rendered-card-reader.test.js
T

369 lines
12 KiB
JavaScript

// 验证 MAIN-world React Fiber reader 只投影订单、询盘与商品的批准字段。
import assert from "node:assert/strict";
import test from "node:test";
import { createOneTalkRenderedCardContentFingerprint } from "@trade-message-center/onetalk-contract";
import { readOneTalkRenderedCard } from "../src/onetalk/main-page/card-observer/react-card-reader.ts";
const imageUrl = "https://img.alicdn.com/card-product.jpg";
const productUrl = "https://chinese.alibaba.com/product-detail/widget-12345.html";
const pageFor = ({ conversationId = "conversation-1", selectionReads = [conversationId] } = {}) => {
let reads = 0;
return {
location: { href: "https://onetalk.alibaba.com/message/default.htm" },
document: {
querySelectorAll: (selector) => {
assert.equal(selector, ".contact-item-container.selected[data-cid]");
const selected = selectionReads[Math.min(reads, selectionReads.length - 1)];
reads += 1;
return selected
? [{ getAttribute: (name) => (name === "data-cid" ? selected : null) }]
: [];
},
},
};
};
const itemFor = (overrides = {}) => ({
messageId: "message-1",
conversationCode: "conversation-1",
messageType: "rec",
sendTime: 1_700_000_000_000,
msgType: 10010,
originalData: { cardType: 9 },
...overrides,
});
const wrapperFor = ({ item = itemFor(), data, cardType } = {}) => {
const templateFiber = {
memoizedProps: {
...(cardType === undefined ? {} : { cardType }),
...(data === undefined ? {} : { data }),
},
};
const fiber = { memoizedProps: { itemData: item }, child: templateFiber };
return {
__reactFiber$fixture: fiber,
querySelectorAll: (selector) => {
assert.equal(selector, "*");
return [];
},
};
};
const readWithFixedTime = (page, wrapper) => {
const previousNow = Date.now;
Date.now = () => 1_700_000_000_123;
try {
return readOneTalkRenderedCard(page, wrapper);
} finally {
Date.now = previousNow;
}
};
test("reads an order that uses productName into the exact normalized card and base evidence", () => {
const content = {
version: 1,
kind: "rendered_order",
title: "Order #1",
products: [{ title: "Widget", imageUrl }],
productCount: 1,
status: { code: "9", text: "Paid" },
payment: { totalDisplay: "$10", discountDisplay: "$1" },
delivery: { shippingAddress: "Hangzhou", methodLabel: "Express", dateLabel: "Tomorrow" },
action: { label: "View order", status: "available" },
};
const read = readWithFixedTime(
pageFor(),
wrapperFor({
data: {
cardTitle: content.title,
productInfoList: [{ productName: "Widget", productImage: imageUrl }],
productCount: 1,
orderStatus: 9,
orderStatusText: "Paid",
shouldPayAmount: "$10",
discountAmount: "$1",
shippingAddress: "Hangzhou",
shippingMethodText: "Express",
deliveryTimeText: "Tomorrow",
orderAction: { label: "View order", status: "available" },
},
}),
);
assert.deepEqual(read, {
observation: {
conversationId: "conversation-1",
messageId: "message-1",
content,
contentFingerprint: createOneTalkRenderedCardContentFingerprint(content),
observedAtMs: 1_700_000_000_123,
},
baseEvidence: {
conversationId: "conversation-1",
messageId: "message-1",
direction: "received",
sentAtMs: 1_700_000_000_000,
},
});
});
test("reads an inquiry into only the approved product, requirement, reference, and ordered action fields", () => {
const content = {
version: 1,
kind: "rendered_inquiry",
product: { title: "Widget", imageUrl },
purchaseQuantity: { value: "100", unit: "pieces" },
requirementText: "Need 100 widgets\nSecond line",
inquiryReference: "inquiry-1",
actions: [
{ label: "Details", available: true },
{ label: "Quote", available: false },
{ label: "Send inquiry", available: true },
],
};
const read = readWithFixedTime(
pageFor(),
wrapperFor({
item: itemFor({ originalData: { cardType: 6 } }),
data: {
inquiryCardDTO: {
summary: "Need 100 widgets\nSecond line",
unapproved: "must not cross",
},
displayProducts: [
{
productName: "Widget",
productImage: imageUrl,
productMOQ: "100",
productUnit: "pieces",
hiddenProductField: "must not cross",
},
],
inquiryContent: "Need 100 widgets\nSecond line",
inquiryID: "inquiry-1",
detailStr: "Details",
detailAction: { params: "must not cross" },
quoteStr: "Quote",
quoteAction: null,
actionName: "Send inquiry",
inquiryAction: { callback: "must not cross" },
},
}),
);
assert.deepEqual(read, {
observation: {
conversationId: "conversation-1",
messageId: "message-1",
content,
contentFingerprint: createOneTalkRenderedCardContentFingerprint(content),
observedAtMs: 1_700_000_000_123,
},
baseEvidence: {
conversationId: "conversation-1",
messageId: "message-1",
direction: "received",
sentAtMs: 1_700_000_000_000,
},
});
});
test("reads a product only through the product renderer and omits every raw or store-image field", () => {
const content = {
version: 1,
kind: "rendered_product",
product: {
title: "Widget",
imageUrl,
sourceUrl: productUrl,
productId: "12345",
},
priceDisplay: "$10",
minimumOrder: { value: "2", unit: "pieces" },
serviceBadges: ["Fast delivery", "Buyer protection"],
};
const read = readWithFixedTime(
pageFor(),
wrapperFor({
item: itemFor({ msgType: 101, originalData: {} }),
cardType: 54,
data: {
title: "Widget",
productImage: imageUrl,
price: "$10",
moq: "2",
moqUnit: "pieces",
productAction: { actionParams: { url: productUrl, unsafe: "must not cross" } },
benefitFirstText: "Fast delivery",
benefitSecondText: "Fast delivery",
promotionTag: "Buyer protection",
hsfImg: "https://img.alicdn.com/store.jpg",
storeImageUrl: "https://img.alicdn.com/store.jpg",
traceInfo: { unsafe: "must not cross" },
},
}),
);
assert.deepEqual(read, {
observation: {
conversationId: "conversation-1",
messageId: "message-1",
content,
contentFingerprint: createOneTalkRenderedCardContentFingerprint(content),
observedAtMs: 1_700_000_000_123,
},
baseEvidence: {
conversationId: "conversation-1",
messageId: "message-1",
direction: "received",
sentAtMs: 1_700_000_000_000,
},
});
assert.deepEqual(Object.keys(read?.observation.content.product ?? {}).sort(), [
"imageUrl",
"productId",
"sourceUrl",
"title",
]);
for (const forbidden of ["storeImageUrl", "hsfImg", "traceInfo", "actionParams", "data"]) {
assert.equal(forbidden in (read?.observation.content.product ?? {}), false);
assert.equal(forbidden in (read?.observation.content ?? {}), false);
}
});
test("rejects files, lookalikes, incomplete templates and identity or session changes", () => {
const orderData = {
cardTitle: "Order #1",
productInfoList: [{ productName: "Widget", productImage: imageUrl }],
orderStatusText: "Paid",
shouldPayAmount: "$10",
shippingAddress: "Hangzhou",
};
const cases = [
{
name: "file card",
page: pageFor(),
wrapper: wrapperFor({
item: itemFor({ originalData: { cardType: 12 } }),
data: orderData,
}),
},
{
name: "unknown 10010 card",
page: pageFor(),
wrapper: wrapperFor({
item: itemFor({ originalData: { cardType: 7 } }),
data: orderData,
}),
},
{
name: "non-product 101 card",
page: pageFor(),
wrapper: wrapperFor({
item: itemFor({ msgType: 101, originalData: {} }),
data: orderData,
}),
},
{
name: "missing template field",
page: pageFor(),
wrapper: wrapperFor({ data: { ...orderData, shippingAddress: undefined } }),
},
{
name: "unsafe image URL",
page: pageFor(),
wrapper: wrapperFor({
data: {
...orderData,
productInfoList: [
{ productName: "Widget", productImage: "https://evil.example/image.jpg" },
],
},
}),
},
{
name: "unsafe product reference",
page: pageFor(),
wrapper: wrapperFor({
item: itemFor({ msgType: 101, originalData: {} }),
cardType: 54,
data: {
title: "Widget",
productImage: imageUrl,
price: "$10",
moq: "2",
moqUnit: "pieces",
productAction: {
actionParams: { url: "https://evil.example/widget-12345.html" },
},
},
}),
},
{
name: "incomplete identity",
page: pageFor(),
wrapper: wrapperFor({ item: itemFor({ messageId: "" }), data: orderData }),
},
{
name: "selection changes while reading",
page: pageFor({ selectionReads: ["conversation-1", "conversation-2"] }),
wrapper: wrapperFor({ data: orderData }),
},
];
for (const entry of cases) {
assert.equal(readWithFixedTime(entry.page, entry.wrapper), null, entry.name);
}
});
test("does not classify a product renderer beyond the bounded Fiber subtree scan", () => {
const data = {
title: "Widget",
productImage: imageUrl,
price: "$10",
moq: "2",
moqUnit: "pieces",
productAction: { actionParams: { url: productUrl } },
};
const fiber = {
memoizedProps: { itemData: itemFor({ msgType: 101, originalData: {} }), data },
};
let cursor = fiber;
for (let index = 0; index < 120; index += 1) {
cursor.sibling = { memoizedProps: {} };
cursor = cursor.sibling;
}
cursor.memoizedProps.cardType = 54;
const wrapper = {
__reactFiber$fixture: fiber,
querySelectorAll: () => [],
};
assert.equal(readWithFixedTime(pageFor(), wrapper), null);
});
test("does not borrow a product renderer from the sibling of the current message root", () => {
const productData = {
title: "Widget",
productImage: imageUrl,
price: "$10",
moq: "2",
moqUnit: "pieces",
productAction: { actionParams: { url: productUrl } },
};
const rootFiber = {
memoizedProps: { itemData: itemFor({ msgType: 101, originalData: {} }) },
sibling: { memoizedProps: { cardType: 54, data: productData } },
};
const wrapper = {
__reactFiber$fixture: rootFiber,
querySelectorAll: () => [],
};
assert.equal(readWithFixedTime(pageFor(), wrapper), null);
});