Files
koc-loop/tests/partner-batch-workbook.test.mjs
2026-08-15 03:53:09 +08:00

328 lines
12 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { strFromU8, strToU8, unzipSync, zipSync } from "fflate";
import { buildRecoveryWorkbook } from "../lib/recovery-workbook.ts";
import {
PARTNER_BATCH_HEADERS,
buildPartnerBatchWorkbookColumns,
parsePartnerBatchWorkbook,
resolvePartnerWorkbookOrigin,
} from "../lib/partner-batch-workbook.ts";
import { hasMp4FileSignature } from "../lib/video-file.ts";
const png = Uint8Array.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
]);
test("round-trips hidden assignment IDs and embedded backfill screenshots", () => {
const workbook = buildRecoveryWorkbook({
sheetName: "批量回填",
headers: [...PARTNER_BATCH_HEADERS],
columnWidths: [10, 20, 40, 30, 40, 24, 28, 20, 20, 20],
hiddenColumns: [7, 8, 9],
rows: [
{
cells: [
1,
"测试笔记",
"正文 #话题",
"",
"https://www.xiaohongshu.com/explore/1234567890abcdef",
"",
"",
"distribution-1",
"",
"",
],
images: [
{
column: 3,
image: { bytes: png, contentType: "image/png", description: "原图" },
},
{
column: 5,
image: { bytes: png, contentType: "image/png", description: "笔记截图" },
},
{
column: 6,
image: { bytes: png, contentType: "image/png", description: "数据截图" },
},
],
},
],
});
const rows = parsePartnerBatchWorkbook(workbook);
assert.equal(rows.length, 1);
assert.equal(rows[0].distributionId, "distribution-1");
assert.equal(rows[0].title, "测试笔记");
assert.match(rows[0].publishUrl, /xiaohongshu\.com/);
assert.equal(rows[0].publishScreenshot?.contentType, "image/png");
assert.equal(rows[0].creatorScreenshot?.contentType, "image/png");
const entries = unzipSync(workbook);
const worksheet = strFromU8(entries["xl/worksheets/sheet1.xml"]);
assert.match(worksheet, /序号(不能改)/);
assert.doesNotMatch(worksheet, /张原图(见图)|已回填(见图)|请插入笔记截图/);
assert.match(worksheet, /<drawing r:id="rId1"\/>/);
assert.doesNotMatch(worksheet, /#VALUE!/);
const drawing = strFromU8(entries["xl/drawings/drawing1.xml"]);
assert.equal((drawing.match(/<xdr:twoCellAnchor editAs="twoCell">/g) ?? []).length, 3);
assert.match(drawing, /<xdr:col>3<\/xdr:col>/);
assert.match(drawing, /<xdr:col>5<\/xdr:col>/);
assert.match(drawing, /<xdr:col>6<\/xdr:col>/);
assert.match(worksheet, /min="8" max="8"[^>]*hidden="1"/);
});
test("accepts the legacy sequence header for previously exported workbooks", () => {
const legacyHeaders = [...PARTNER_BATCH_HEADERS];
legacyHeaders[0] = "序号";
const workbook = buildRecoveryWorkbook({
sheetName: "批量回填",
headers: legacyHeaders,
columnWidths: [10, 20, 40, 30, 40, 24, 28, 20, 20, 20],
hiddenColumns: [7, 8, 9],
rows: [
{
cells: [
1,
"旧模板笔记",
"正文",
"",
"",
"",
"",
"distribution-legacy",
"",
"",
],
images: [],
},
],
});
const rows = parsePartnerBatchWorkbook(workbook);
assert.equal(rows[0].sequence, "1");
assert.equal(rows[0].distributionId, "distribution-legacy");
});
test("imports dynamic source image columns without confusing screenshot columns", () => {
const headers = [
"序号(不能改)",
"标题",
"笔记内容(正文+话题)",
"图片1",
"图片2",
"发布链接",
"笔记截图",
"数据分析截图(单篇笔记数据分析截图)",
"_系统笔记ID",
"_原笔记截图",
"_原数据分析截图",
];
const workbook = buildRecoveryWorkbook({
sheetName: "批量回填",
headers,
columnWidths: headers.map(() => 20),
hiddenColumns: [8, 9, 10],
rows: [
{
cells: [
1,
"多图笔记",
"正文",
"",
"",
"https://www.xiaohongshu.com/explore/dynamic",
"",
"",
"distribution-dynamic",
"",
"",
],
images: [
{
column: 3,
image: { bytes: png, contentType: "image/png", description: "原图1" },
},
{
column: 4,
image: { bytes: png, contentType: "image/png", description: "原图2" },
},
{
column: 6,
image: { bytes: png, contentType: "image/png", description: "笔记截图" },
},
{
column: 7,
image: { bytes: png, contentType: "image/png", description: "数据截图" },
},
],
},
],
});
const rows = parsePartnerBatchWorkbook(workbook);
assert.equal(rows[0].distributionId, "distribution-dynamic");
assert.equal(rows[0].publishScreenshot?.contentType, "image/png");
assert.equal(rows[0].creatorScreenshot?.contentType, "image/png");
});
test("imports screenshots saved by WPS as DISPIMG cell images", () => {
const sourceImage = Uint8Array.from([...png, 1]);
const publishScreenshot = Uint8Array.from([...png, 2]);
const creatorScreenshot = Uint8Array.from([...png, 3]);
const worksheet = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="1">
<c r="A1" t="inlineStr"><is><t>序号(不能改)</t></is></c>
<c r="B1" t="inlineStr"><is><t>标题</t></is></c>
<c r="C1" t="inlineStr"><is><t>笔记内容(正文+话题)</t></is></c>
<c r="D1" t="inlineStr"><is><t>图片1</t></is></c>
<c r="E1" t="inlineStr"><is><t>发布链接</t></is></c>
<c r="F1" t="inlineStr"><is><t>笔记截图</t></is></c>
<c r="G1" t="inlineStr"><is><t>数据分析截图(单篇笔记数据分析截图)</t></is></c>
<c r="H1" t="inlineStr"><is><t>_系统笔记ID</t></is></c>
<c r="I1" t="inlineStr"><is><t>_原笔记截图</t></is></c>
<c r="J1" t="inlineStr"><is><t>_原数据分析截图</t></is></c>
</row>
<row r="2">
<c r="A2"><v>1</v></c>
<c r="B2" t="inlineStr"><is><t>WPS 笔记</t></is></c>
<c r="D2" t="str"><f>_xlfn.DISPIMG(&quot;SOURCE&quot;,1)</f><v>=DISPIMG(&quot;SOURCE&quot;,1)</v></c>
<c r="E2" t="inlineStr"><is><t>https://www.xiaohongshu.com/explore/wps</t></is></c>
<c r="F2" t="str"><f>_xlfn.DISPIMG(&quot;PUBLISH&quot;,1)</f><v>=DISPIMG(&quot;PUBLISH&quot;,1)</v></c>
<c r="G2" t="str"><f>_xlfn.DISPIMG(&quot;CREATOR&quot;,1)</f><v>=DISPIMG(&quot;CREATOR&quot;,1)</v></c>
<c r="H2" t="inlineStr"><is><t>distribution-wps</t></is></c>
</row>
</sheetData>
</worksheet>`;
const cellImages = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<etc:cellImages xmlns:etc="http://www.wps.cn/officeDocument/2017/etCustomData" xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<etc:cellImage><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="1" name="SOURCE"/></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId1"/></xdr:blipFill></xdr:pic></etc:cellImage>
<etc:cellImage><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="2" name="PUBLISH"/></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId2"/></xdr:blipFill></xdr:pic></etc:cellImage>
<etc:cellImage><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="3" name="CREATOR"/></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId3"/></xdr:blipFill></xdr:pic></etc:cellImage>
</etc:cellImages>`;
const relationships = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/source.png"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/publish.png"/>
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/creator.png"/>
</Relationships>`;
const workbook = zipSync({
"xl/worksheets/sheet1.xml": strToU8(worksheet),
"xl/cellimages.xml": strToU8(cellImages),
"xl/_rels/cellimages.xml.rels": strToU8(relationships),
"xl/media/source.png": sourceImage,
"xl/media/publish.png": publishScreenshot,
"xl/media/creator.png": creatorScreenshot,
});
const rows = parsePartnerBatchWorkbook(workbook);
assert.equal(rows.length, 1);
assert.equal(rows[0].distributionId, "distribution-wps");
assert.deepEqual(rows[0].publishScreenshot?.bytes, publishScreenshot);
assert.deepEqual(rows[0].creatorScreenshot?.bytes, creatorScreenshot);
});
test("builds video-task workbooks with video columns and no source image columns", () => {
const columns = buildPartnerBatchWorkbookColumns({
contentFormat: "video",
maxSourceImages: 3,
maxSourceVideos: 2,
});
assert.deepEqual(columns.headers.slice(0, 8), [
"序号(不能改)",
"标题",
"笔记内容(正文+话题)",
"视频1",
"视频2",
"发布链接",
"笔记截图",
"数据分析截图(单篇笔记数据分析截图)",
]);
assert.equal(columns.headers.some((header) => /^图片\d+$/.test(header)), false);
const workbook = buildRecoveryWorkbook({
sheetName: "视频批量回填",
headers: columns.headers,
columnWidths: columns.columnWidths,
hiddenColumns: [
columns.systemColumn,
columns.systemColumn + 1,
columns.systemColumn + 2,
],
rows: [
{
cells: [
1,
"视频笔记",
"视频正文 #测试",
"下载视频1",
"下载视频2",
"",
"",
"",
"distribution-video",
"",
"",
],
images: [],
hyperlinks: [
{
column: columns.sourceVideoStartColumn,
url: "https://koc.example.com/api/partner-image?kind=video&download=1",
},
],
},
],
});
const entries = unzipSync(workbook);
const worksheet = strFromU8(entries["xl/worksheets/sheet1.xml"]);
const relationships = strFromU8(
entries["xl/worksheets/_rels/sheet1.xml.rels"],
);
assert.match(worksheet, /视频1/);
assert.doesNotMatch(worksheet, /图片1/);
assert.match(relationships, /https:\/\/koc\.example\.com\/api\/partner-image/);
assert.match(relationships, /download=1/);
});
test("uses the configured public origin before proxy or container addresses", () => {
const request = new Request("http://app:3000/api/partner-batch-workbook", {
headers: {
host: "app:3000",
"x-forwarded-host": "internal-proxy:8080",
"x-forwarded-proto": "https",
},
});
assert.equal(
resolvePartnerWorkbookOrigin(request, "https://koc.example.com/koc/"),
"https://koc.example.com",
);
});
test("preserves a forwarded non-standard port when no origin is configured", () => {
const request = new Request("http://app:3000/api/partner-batch-workbook", {
headers: {
host: "app:3000",
"x-forwarded-host": "localhost:8080",
"x-forwarded-proto": "http",
},
});
assert.equal(resolvePartnerWorkbookOrigin(request), "http://localhost:8080");
});
test("recognizes MP4 bytes instead of trusting a response content type", () => {
const mp4Header = Uint8Array.from([
0x00, 0x00, 0x00, 0x18,
0x66, 0x74, 0x79, 0x70,
0x69, 0x73, 0x6f, 0x6d,
0x00, 0x00, 0x02, 0x00,
0x69, 0x73, 0x6f, 0x6d,
0x6d, 0x70, 0x34, 0x32,
]);
assert.equal(hasMp4FileSignature(mp4Header), true);
assert.equal(hasMp4FileSignature(new TextEncoder().encode("not a video")), false);
});