mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
369 lines
12 KiB
JavaScript
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);
|
|
});
|