Files
trade-message-center/apps/chrome-extension/test/onetalk-image-send.test.js
T

447 lines
16 KiB
JavaScript

// 验证 MAIN 图片下载、上传回调隔离、生命周期围栏与 45 秒终态。
import assert from "node:assert/strict";
import test from "node:test";
import { sendOneTalkImage } from "../src/onetalk/main-page/image-send.ts";
import { createSendObservationCorrelator } from "../src/onetalk/main-page/message-observer/send-observation.ts";
const createClock = () => {
let now = 0;
let sequence = 0;
const timers = new Map();
const schedule = (callback, delay) => {
const id = ++sequence;
timers.set(id, { callback, due: now + delay });
return id;
};
const cancel = (id) => timers.delete(id);
const advance = (duration) => {
now += duration;
for (const [id, timer] of [...timers]) {
if (timer.due > now) continue;
timers.delete(id);
timer.callback();
}
};
return { now: () => now, schedule, cancel, advance };
};
const createPageWindow = () => ({
location: { href: "https://onetalk.alibaba.com/message/weblitePWA.htm" },
addEventListener: () => {},
currentUserAccountId: "seller-account",
__conversationListFullData__: [
{
cid: "conversation-1",
accountId: "buyer-account-1",
aliId: "buyer-ali-1",
contact: { aliId: "buyer-ali-1" },
owner: { accountId: "seller-account", aliId: "seller-ali" },
},
{
cid: "conversation-2",
accountId: "buyer-account-2",
aliId: "buyer-ali-2",
contact: { aliId: "buyer-ali-2" },
owner: { accountId: "seller-account", aliId: "seller-ali" },
},
],
});
const imageSource = (name = "image.jpg") => ({
kind: "image",
source: {
downloadUrl: "https://mind.example.test/bridge/image?temporary=secret",
fileName: name,
mimeType: "image/jpeg",
},
});
const observedImage = (conversationId, messageId, fileId) => ({
messageType: "new",
upstreamType: 1,
messageId,
conversationId,
senderId: "seller@icbu",
direction: "sent",
sentAtMs: 1,
content: {
version: 1,
kind: "image",
fileId,
extension: "jpg",
sizeBytes: 123,
isOriginal: true,
md5: `hash-${fileId}`,
previewUrl: null,
urlScope: "onetalk_session",
},
participantIds: ["buyer@icbu", "seller@icbu"],
readStatus: 0,
messageStatus: 1,
unreadCount: 0,
});
// Raw-only relation callback fixture: dimensions are hostile input and must not enter expected metadata.
const metadata = (fileId) => ({
mediaInfo: {
nodeSize: 123,
width: "ignored-raw-value",
height: -1,
md5: `hash-${fileId}`,
fileId,
},
});
const relationMetadata = (fileId) => ({
nodeSize: 123,
md5: `hash-${fileId}`,
id: 4_567_890,
materialType: "image",
});
const createUploader = () => {
const uploads = [];
const owner = {
sendFile: () => undefined,
sendFileToOss: (input) => {
uploads.push(input);
return undefined;
},
};
return {
uploads,
owner,
uploader: { owner, sendFile: owner.sendFile, sendFileToOss: owner.sendFileToOss },
};
};
test("confirms an actual pending image send from a matching no-dimension live observation", async () => {
const originalFetch = globalThis.fetch;
const pageWindow = createPageWindow();
const clock = createClock();
const { uploader, owner, uploads } = createUploader();
const correlator = createSendObservationCorrelator({ ...clock, textTimeoutMs: 10 });
globalThis.fetch = async () => new Response(new Uint8Array([1, 2, 3]), { status: 200 });
try {
const result = sendOneTalkImage({
pageWindow,
conversationId: "conversation-2",
content: imageSource("final-name.jpg"),
sendObservation: correlator,
findUploader: () => uploader,
runtime: clock,
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(uploads.length, 1);
assert.equal(uploads[0].file.name, "final-name.jpg");
assert.equal(uploads[0].file.type, "image/jpeg");
assert.equal(uploads[0].contact.cid, "conversation-2");
assert.deepEqual(uploads[0].fromTo, {
from: "seller-account",
to: "buyer-account-2",
fromAliId: "seller-ali",
toAliId: "buyer-ali-2",
});
// Production OneTalk calls sendFile(relationInfo, tmpKey, conversation, callback).
// relationInfo has nodeSize + md5 but no image dimensions.
void owner.sendFile(
relationMetadata("file-2"),
uploads[0].tmpKey,
{ cid: "conversation-2" },
() => undefined,
);
correlator.observe([observedImage("conversation-2", "message-2", "file-2")]);
const value = await result;
assert.equal(value.status, "confirmed_sent");
assert.equal(JSON.stringify(value).includes("mind.example.test"), false);
assert.equal(JSON.stringify(value).includes("temporary=secret"), false);
} finally {
globalThis.fetch = originalFetch;
}
});
test("binds default deadline timers to the page global instead of the runtime record", async () => {
const originalFetch = globalThis.fetch;
const originalSetTimeout = globalThis.setTimeout;
const originalClearTimeout = globalThis.clearTimeout;
const pageWindow = createPageWindow();
const clock = createClock();
const { uploader, owner, uploads } = createUploader();
const receivers = [];
globalThis.fetch = async () => new Response(new Uint8Array([1]), { status: 200 });
globalThis.setTimeout = function (...args) {
receivers.push(this);
return originalSetTimeout.apply(globalThis, args);
};
globalThis.clearTimeout = function (...args) {
return originalClearTimeout.apply(globalThis, args);
};
try {
const correlator = createSendObservationCorrelator({ ...clock, textTimeoutMs: 10 });
const result = sendOneTalkImage({
pageWindow,
conversationId: "conversation-1",
content: imageSource(),
sendObservation: correlator,
findUploader: () => uploader,
});
await new Promise((resolve) => setImmediate(resolve));
void owner.sendFile(uploads[0].tmpKey, metadata("file-1"));
correlator.observe([observedImage("conversation-1", "message-1", "file-1")]);
assert.equal((await result).status, "confirmed_sent");
assert.equal(
receivers.every((receiver) => receiver === globalThis),
true,
);
} finally {
globalThis.fetch = originalFetch;
globalThis.setTimeout = originalSetTimeout;
globalThis.clearTimeout = originalClearTimeout;
}
});
test("keeps concurrent different-conversation uploads isolated by tmpKey", async () => {
const originalFetch = globalThis.fetch;
const pageWindow = createPageWindow();
const clock = createClock();
const { uploader, owner, uploads } = createUploader();
const correlator = createSendObservationCorrelator({ ...clock, textTimeoutMs: 10 });
globalThis.fetch = async () => new Response(new Uint8Array([1]), { status: 200 });
try {
const first = sendOneTalkImage({
pageWindow,
conversationId: "conversation-1",
content: imageSource(),
sendObservation: correlator,
findUploader: () => uploader,
runtime: clock,
});
const second = sendOneTalkImage({
pageWindow,
conversationId: "conversation-2",
content: imageSource(),
sendObservation: correlator,
findUploader: () => uploader,
runtime: clock,
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(uploads.length, 2);
void owner.sendFile(uploads[1].tmpKey, metadata("file-2"));
correlator.observe([observedImage("conversation-2", "message-2", "file-2")]);
void owner.sendFile(uploads[0].tmpKey, metadata("file-1"));
correlator.observe([observedImage("conversation-1", "message-1", "file-1")]);
assert.equal((await first).status, "confirmed_sent");
assert.equal((await second).status, "confirmed_sent");
} finally {
globalThis.fetch = originalFetch;
}
});
test("uses a deterministic 45-second image budget rather than the text observer timeout", async () => {
const originalFetch = globalThis.fetch;
const pageWindow = createPageWindow();
const clock = createClock();
const { uploader } = createUploader();
const correlator = createSendObservationCorrelator({ ...clock, textTimeoutMs: 1 });
globalThis.fetch = async () => new Response(new Uint8Array([1]), { status: 200 });
try {
let settled = false;
const result = sendOneTalkImage({
pageWindow,
conversationId: "conversation-1",
content: imageSource(),
sendObservation: correlator,
findUploader: () => uploader,
runtime: clock,
}).then((value) => {
settled = true;
return value;
});
await new Promise((resolve) => setImmediate(resolve));
clock.advance(10_000);
await Promise.resolve();
assert.equal(settled, false);
clock.advance(35_000);
assert.deepEqual(await result, { status: "delivery_unknown", reason: "send_timeout" });
} finally {
globalThis.fetch = originalFetch;
}
});
test("stops a source fetch held beyond the MAIN defensive command deadline before upload", async () => {
const originalFetch = globalThis.fetch;
const pageWindow = createPageWindow();
const clock = createClock();
const { uploader, uploads } = createUploader();
globalThis.fetch = () => new Promise(() => {});
try {
const result = sendOneTalkImage({
pageWindow,
conversationId: "conversation-1",
content: imageSource(),
sendObservation: createSendObservationCorrelator({ ...clock, textTimeoutMs: 1 }),
findUploader: () => uploader,
runtime: clock,
});
clock.advance(45_000);
assert.deepEqual(await result, { status: "delivery_unknown", reason: "send_timeout" });
assert.deepEqual(uploads, []);
} finally {
globalThis.fetch = originalFetch;
}
});
test("invokes fetch for rejection and non-2xx, then fails closed before native upload", async () => {
const originalFetch = globalThis.fetch;
const pageWindow = createPageWindow();
const { uploader, uploads } = createUploader();
let calls = 0;
try {
globalThis.fetch = async () => {
calls += 1;
throw new Error("network");
};
assert.deepEqual(
await sendOneTalkImage({
pageWindow,
conversationId: "conversation-1",
content: imageSource(),
sendObservation: createSendObservationCorrelator(),
findUploader: () => uploader,
}),
{ status: "delivery_unknown", reason: "send_connection_lost" },
);
globalThis.fetch = async () => {
calls += 1;
return new Response(null, { status: 403 });
};
assert.deepEqual(
await sendOneTalkImage({
pageWindow,
conversationId: "conversation-1",
content: imageSource(),
sendObservation: createSendObservationCorrelator(),
findUploader: () => uploader,
}),
{ status: "delivery_unknown", reason: "send_connection_lost" },
);
assert.equal(calls, 2);
assert.deepEqual(uploads, []);
} finally {
globalThis.fetch = originalFetch;
}
});
test("normalizes a synchronous native uploader throw and cleans up its pending command", async () => {
const originalFetch = globalThis.fetch;
const pageWindow = createPageWindow();
const { owner, uploader } = createUploader();
owner.sendFileToOss = () => {
throw new Error("native-uploader-throw");
};
uploader.sendFileToOss = owner.sendFileToOss;
globalThis.fetch = async () => new Response(new Uint8Array([1]), { status: 200 });
try {
assert.deepEqual(
await sendOneTalkImage({
pageWindow,
conversationId: "conversation-1",
content: imageSource(),
sendObservation: createSendObservationCorrelator(),
findUploader: () => uploader,
}),
{ status: "delivery_unknown", reason: "send_connection_lost" },
);
} finally {
globalThis.fetch = originalFetch;
}
});
test("fails closed when the logged-in account changes while the source fetch is pending", async () => {
const originalFetch = globalThis.fetch;
const pageWindow = createPageWindow();
const { uploader, uploads } = createUploader();
let release;
globalThis.fetch = () =>
new Promise((resolve) => {
release = () => resolve(new Response(new Uint8Array([1]), { status: 200 }));
});
try {
const result = sendOneTalkImage({
pageWindow,
conversationId: "conversation-1",
content: imageSource(),
sendObservation: createSendObservationCorrelator(),
findUploader: () => uploader,
});
pageWindow.currentUserAccountId = "other-seller";
release();
assert.deepEqual(await result, {
status: "delivery_unknown",
reason: "send_connection_lost",
});
assert.deepEqual(uploads, []);
} finally {
globalThis.fetch = originalFetch;
}
});
test("fences target identity after Blob read and before the irreversible native send", async () => {
const originalFetch = globalThis.fetch;
const pageWindow = createPageWindow();
const { uploader, owner, uploads } = createUploader();
let releaseBlob;
let originalSendCalls = 0;
owner.sendFile = () => {
originalSendCalls += 1;
return undefined;
};
uploader.sendFile = owner.sendFile;
globalThis.fetch = async () => ({
ok: true,
blob: () =>
new Promise((resolve) => {
releaseBlob = () => resolve(new Blob([new Uint8Array([1])]));
}),
});
try {
const blobBoundary = sendOneTalkImage({
pageWindow,
conversationId: "conversation-1",
content: imageSource(),
sendObservation: createSendObservationCorrelator(),
findUploader: () => uploader,
});
await new Promise((resolve) => setImmediate(resolve));
pageWindow.currentUserAccountId = "other-seller";
releaseBlob();
assert.deepEqual(await blobBoundary, {
status: "delivery_unknown",
reason: "send_connection_lost",
});
assert.deepEqual(uploads, []);
pageWindow.currentUserAccountId = "seller-account";
globalThis.fetch = async () => new Response(new Uint8Array([1]), { status: 200 });
const nativeBoundary = sendOneTalkImage({
pageWindow,
conversationId: "conversation-1",
content: imageSource(),
sendObservation: createSendObservationCorrelator(),
findUploader: () => uploader,
});
await new Promise((resolve) => setImmediate(resolve));
pageWindow.currentUserAccountId = "other-seller";
void owner.sendFile(uploads[0].tmpKey, metadata("file-1"));
assert.deepEqual(await nativeBoundary, {
status: "delivery_unknown",
reason: "send_connection_lost",
});
assert.equal(originalSendCalls, 0);
} finally {
globalThis.fetch = originalFetch;
}
});