cangku/backend/test/alibaba1688ProductService.test.js

304 lines
11 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const {
normalizeAlibaba1688Product,
normalizeAlibaba1688ImageUrl,
mapProductRow,
listAlibaba1688Products,
replaceAlibaba1688Product,
} = require('../src/services/alibaba1688ProductService');
const planningDatabase = require('../src/services/planningDatabase');
function product(overrides = {}) {
return {
productId: '1073252781818',
title: '圆桌测试商品',
sourceUrl: 'https://detail.1688.com/offer/1073252781818.html',
mainImages: ['https://img.example.com/main.jpg'],
detailImages: ['https://img.example.com/detail.jpg'],
videos: [{
url: 'https://cloud.video.taobao.com/play/example.mp4',
coverUrl: 'https://img.example.com/video-cover.jpg_sum.jpg',
duration: 20.4,
width: 720,
height: 1280,
source: 'main-gallery',
}],
skuDimensions: [
{ name: '颜色', values: ['原木色', '胡桃色'] },
{ name: '尺寸', values: ['60*60', '70*70'] },
],
skus: [{
name: '原木色 / 60*60',
specs: [{ name: '颜色', value: '原木色' }, { name: '尺寸', value: '60*60' }],
image: 'https://img.example.com/sku-original-60.jpg',
price: 180,
freight: 40,
}],
attributes: [{ name: '材质', value: '实木' }],
storeCategory: '鞋架/鞋柜',
platformCategory: '家装建材 > 简易家具',
dropshipSupported: true,
importTag: '测款',
collectedAt: '2026-08-14T02:13:08.923Z',
...overrides,
};
}
test('normalizes a complete 1688 snapshot for full server replacement', () => {
const normalized = normalizeAlibaba1688Product({
dingtalkUserName: '香茗',
sourceType: 'product',
sourceImportId: 'import-1',
product: product(),
}, '1073252781818');
assert.equal(normalized.productId, '1073252781818');
assert.equal(normalized.detailImageCount, 1);
assert.equal(normalized.skuCount, 1);
assert.equal(normalized.snapshot.skuDimensions.length, 2);
assert.deepEqual(normalized.snapshot.skuImages, ['https://img.example.com/sku-original-60.jpg']);
assert.equal(normalized.snapshot.skus[0].image, 'https://img.example.com/sku-original-60.jpg');
assert.deepEqual(normalized.snapshot.videos, [{
url: 'https://cloud.video.taobao.com/play/example.mp4',
coverUrl: 'https://img.example.com/video-cover.jpg',
duration: 20.4,
width: 720,
height: 1280,
mimeType: '',
source: 'main-gallery',
}]);
assert.equal(normalized.snapshot.schemaVersion, 2);
assert.match(normalized.skuFingerprint, /^[a-f0-9]{64}$/);
assert.equal(normalized.snapshot.storeCategory, '鞋架/鞋柜');
assert.equal(normalized.snapshot.platformCategory, '家装建材 > 简易家具');
assert.equal(normalized.snapshot.dropshipSupported, true);
assert.deepEqual(normalized.snapshot.skus[0].specs, [
{ name: '颜色', value: '原木色' },
{ name: '尺寸', value: '60*60' },
]);
assert.equal(normalized.syncOperatorName, '香茗');
});
test('stores original Alibaba SKU image URLs instead of _sum thumbnails', () => {
const compressed = 'https://cbu01.alicdn.com/img/ibank/example.jpg_sum.jpg';
const original = 'https://cbu01.alicdn.com/img/ibank/example.jpg';
const normalized = normalizeAlibaba1688Product({
dingtalkUserName: '香茗',
sourceType: 'product',
product: product({
images: [compressed],
mainImages: [compressed],
detailImages: [compressed],
skuImages: [compressed],
skus: [{ name: '原木色', image: compressed }],
baseSkuRows: [{ name: '原木色', image: compressed }],
}),
});
assert.equal(normalizeAlibaba1688ImageUrl(`${compressed}?x=1`), `${original}?x=1`);
assert.deepEqual(normalized.snapshot.images, [original]);
assert.deepEqual(normalized.snapshot.mainImages, [original]);
assert.deepEqual(normalized.snapshot.detailImages, [original]);
assert.deepEqual(normalized.snapshot.skuImages, [original]);
assert.equal(normalized.snapshot.skus[0].image, original);
assert.equal(normalized.snapshot.baseSkuRows[0].image, original);
});
test('promotes videos preserved by older servers in skuScan metadata', () => {
const normalized = normalizeAlibaba1688Product({
product: product({
videos: [],
skuScan: {
complete: true,
videos: [{
src: 'https://cloud.video.taobao.com/play/legacy.mp4',
poster: 'https://img.example.com/legacy-cover.jpg',
duration: 18,
}],
},
}),
});
assert.equal(normalized.snapshot.videos.length, 1);
assert.equal(normalized.snapshot.videos[0].url, 'https://cloud.video.taobao.com/play/legacy.mp4');
});
test('rejects incomplete snapshots and mismatched route IDs', () => {
assert.throws(
() => normalizeAlibaba1688Product({ product: product({ detailImages: [] }) }),
error => error.statusCode === 422 && error.code === 'DETAIL_IMAGES_REQUIRED'
);
assert.throws(
() => normalizeAlibaba1688Product({ product: product() }, 'different-id'),
error => error.statusCode === 409 && error.code === 'PRODUCT_ID_MISMATCH'
);
});
test('maps server rows without exposing the large snapshot in list mode', () => {
const row = {
product_id: '1073252781818',
title: '圆桌测试商品',
product_snapshot_json: JSON.stringify(product()),
main_image_count: 16,
detail_image_count: 24,
sku_count: 66,
attribute_count: 49,
sync_version: 3,
};
const summary = mapProductRow(row, false);
const detail = mapProductRow(row, true);
assert.equal(summary.product, undefined);
assert.equal(summary.skuCount, 66);
assert.equal(detail.product.productId, '1073252781818');
assert.equal(detail.syncVersion, 3);
});
test('lists 1688 products with multi-status filtering and unfiltered status summary', async () => {
const originalQuery = planningDatabase.query;
const queries = [];
planningDatabase.query = async (sql, params = []) => {
queries.push({ sql, params });
if (sql.includes('information_schema.tables')) return [{ exists: 1 }];
if (sql.includes('COUNT(*) AS total') && !sql.includes('GROUP BY')) return [{ total: 2 }];
if (sql.includes('GROUP BY publish_status')) {
return [
{ publish_status: 'pending', total: 4, latest_updated_at: '2026-08-14 10:00:00' },
{ publish_status: 'ready', total: 2, latest_updated_at: '2026-08-14 11:00:00' },
{ publish_status: 'published', total: 3, latest_updated_at: '2026-08-13 12:00:00' },
];
}
return [{
product_id: '1073252781818',
title: '圆桌测试商品',
publish_status: 'ready',
sync_version: 2,
}];
};
try {
const result = await listAlibaba1688Products({
keyword: '圆桌',
importTag: '测款',
sourceType: 'product',
syncOperatorName: '香茗',
publishStatus: 'pending,ready',
page: 2,
pageSize: 10,
});
assert.equal(result.total, 2);
assert.equal(result.items.length, 1);
assert.equal(result.summary.pending, 4);
assert.equal(result.summary.ready, 2);
assert.equal(result.summary.published, 3);
assert.equal(result.summary.failed, 0);
assert.equal(result.latestUpdatedAt, '2026-08-14 11:00:00');
const countQuery = queries.find(query => query.sql.includes('COUNT(*) AS total') && !query.sql.includes('GROUP BY'));
assert.match(countQuery.sql, /publish_status IN \(\?, \?\)/);
assert.deepEqual(countQuery.params.slice(-2), ['pending', 'ready']);
const summaryQuery = queries.find(query => query.sql.includes('GROUP BY publish_status'));
assert.doesNotMatch(summaryQuery.sql, /publish_status IN/);
assert.deepEqual(summaryQuery.params.slice(-3), ['测款', 'product', '%香茗%']);
const listQuery = queries.find(query => query.sql.includes('ORDER BY updated_at DESC'));
assert.deepEqual(listQuery.params.slice(-4), ['pending', 'ready', 10, 10]);
} finally {
planningDatabase.query = originalQuery;
}
});
test('paginates 1688 products and applies common server-side filters', async () => {
const originalQuery = planningDatabase.query;
const calls = [];
planningDatabase.query = async (sql, params = []) => {
calls.push({ sql, params });
if (sql.includes('information_schema.tables')) return [{ exists: 1 }];
if (sql.includes('COUNT(*) AS total') && !sql.includes('GROUP BY')) return [{ total: 51 }];
if (sql.includes('GROUP BY publish_status')) return [];
return [{
product_id: '1073252781818',
title: '圆桌测试商品',
store_name: '测试家具厂',
category: '餐厅家具',
import_tag: '测款',
publish_status: 'pending',
sync_operator_name: '香茗',
source_type: 'store',
product_snapshot_json: JSON.stringify(product()),
}];
};
try {
const result = await listAlibaba1688Products({
page: 2,
pageSize: 50,
keyword: '桌',
importTag: '测款',
publishStatus: 'pending',
syncOperatorName: '香茗',
sourceType: 'store',
});
const countCall = calls.find(call => call.sql.includes('COUNT(*) AS total'));
const listCall = calls.find(call => call.sql.includes('ORDER BY updated_at DESC'));
assert.equal(result.page, 2);
assert.equal(result.pageSize, 50);
assert.equal(result.total, 51);
assert.equal(result.items[0].syncOperatorName, '香茗');
assert.match(countCall.sql, /category LIKE \?/);
assert.match(countCall.sql, /sync_operator_name LIKE \?/);
assert.match(countCall.sql, /source_type = \?/);
assert.deepEqual(countCall.params, [
'%桌%', '%桌%', '%桌%', '%桌%', '%桌%', '测款', 'store', '%香茗%', 'pending',
]);
assert.deepEqual(listCall.params.slice(-2), [50, 50]);
} finally {
planningDatabase.query = originalQuery;
}
});
test('replaces the complete server snapshot without resetting publish workflow state', async () => {
const originalQuery = planningDatabase.query;
const originalExecute = planningDatabase.execute;
const executed = [];
planningDatabase.query = async (sql) => {
if (sql.includes('information_schema.tables')) return [{ exists: 1 }];
return [{
product_id: '1073252781818',
title: '新标题',
product_snapshot_json: JSON.stringify(product({ title: '新标题' })),
sync_version: 3,
}];
};
planningDatabase.execute = async (sql, params) => {
executed.push({ sql, params });
return { affectedRows: 2 };
};
try {
const result = await replaceAlibaba1688Product(
'1073252781818',
{ product: product({ title: '新标题' }) },
{ apiKeyId: 'key-id', apiKeyName: 'desktop' }
);
assert.equal(result.operation, 'replaced');
assert.equal(result.item.title, '新标题');
assert.equal(executed.length, 1);
assert.equal(executed[0].params.length, 23);
assert.doesNotMatch(executed[0].sql, /publish_status = 'pending'/);
assert.doesNotMatch(executed[0].sql, /published_goods_json = NULL/);
assert.doesNotMatch(executed[0].sql, /last_published_at = NULL/);
assert.match(executed[0].sql, /sync_version = sync_version \+ 1/);
} finally {
planningDatabase.query = originalQuery;
planningDatabase.execute = originalExecute;
}
});