import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import type { AddressInfo } from 'node:net'; import type { Database } from '@atlas/database'; import type { NestExpressApplication } from '@nestjs/platform-express'; import { afterAll, beforeAll, describe, it } from 'vitest'; import { DATABASE } from '../src/database/database.module.js'; import { createApplication } from '../src/main.js'; import { PRODUCT_VARIANT_LIMIT } from '@atlas/contracts'; let app: NestExpressApplication; let database: Database; let baseUrl = ''; let workspaceId = ''; let userId = ''; let webAccessToken = ''; async function request(path: string, init: RequestInit = {}) { const response = await fetch(`${baseUrl}${path}`, { redirect: 'manual', ...init, headers: { 'Content-Type': 'application/json', ...init.headers }, }); return { response, body: response.status === 204 || response.status === 302 ? undefined : ((await response.json()) as any), }; } describe('扩展授权与采集 HTTP 集成测试', () => { beforeAll(async () => { Object.assign(process.env, { NODE_ENV: 'test', SWAGGER_ENABLED: 'false', AUTH_REGISTER_IP_LIMIT: '100', AUTH_REGISTER_EMAIL_LIMIT: '100', }); const created = await createApplication(); app = created.app; database = app.get(DATABASE); await app.listen(0, '127.0.0.1'); baseUrl = `http://127.0.0.1:${(app.getHttpServer().address() as AddressInfo).port}`; }); afterAll(async () => { await database.workspaceTransaction(workspaceId, async (tx) => { await tx.query( 'DELETE FROM atlas.idempotency_requests WHERE workspace_id = $1', [workspaceId], ); await tx.query( 'DELETE FROM atlas.outbox_events WHERE workspace_id = $1', [workspaceId], ); await tx.query( 'DELETE FROM atlas.operations WHERE workspace_id = $1', [workspaceId], ); await tx.query( 'DELETE FROM atlas.collection_asset_transfers WHERE workspace_id = $1', [workspaceId], ); await tx.query( 'DELETE FROM atlas.product_variant_platform_listings WHERE workspace_id = $1', [workspaceId], ); await tx.query( 'DELETE FROM atlas.product_platform_profiles WHERE workspace_id = $1', [workspaceId], ); await tx.query('DELETE FROM atlas.stores WHERE workspace_id = $1', [ workspaceId, ]); await tx.query( 'DELETE FROM atlas.product_variants WHERE workspace_id = $1', [workspaceId], ); await tx.query( 'DELETE FROM atlas.products WHERE workspace_id = $1', [workspaceId], ); await tx.query( 'DELETE FROM atlas.collection_items WHERE workspace_id = $1', [workspaceId], ); await tx.query( 'DELETE FROM atlas.extension_authorization_codes WHERE workspace_id = $1', [workspaceId], ); await tx.query( 'DELETE FROM atlas.extension_sessions WHERE workspace_id = $1', [workspaceId], ); }); await database.query( 'DELETE FROM atlas.workspace_memberships WHERE user_id = $1', [userId], ); await database.query('DELETE FROM atlas.workspaces WHERE id = $1', [ workspaceId, ]); await database.query('DELETE FROM atlas.users WHERE id = $1', [userId]); await app.close(); }); it('未登录令牌返回可操作的扩展会话错误', async () => { const session = await request('/api/extension/session'); assert.equal(session.response.status, 401); assert.equal(session.body.error.code, 'SESSION_EXPIRED'); assert.equal( session.body.error.message, 'Atlas ERP 登录已过期,请重新登录', ); }); it('通过 Atlas Web access token 写入来源商品,并以新快照覆盖重复采集', async () => { const registration = await request('/api/auth/register', { method: 'POST', body: JSON.stringify({ email: `extension-${randomUUID()}@example.test`, password: 'correct-horse-battery-staple-123', password_confirmation: 'correct-horse-battery-staple-123', }), }); assert.equal(registration.response.status, 201); userId = registration.body.data.user.id; workspaceId = registration.body.data.workspace.id; webAccessToken = registration.body.data.access_token as string; const session = await request('/api/extension/session', { headers: { Authorization: `Bearer ${webAccessToken}` }, }); assert.equal(session.response.status, 200); assert.equal(session.body.data.workspace.id, workspaceId); const payload = { source: { platform: 'ozon', itemKey: '123456789', canonicalUrl: 'https://www.ozon.ru/product/demo-123456789/', collectedAt: new Date().toISOString(), parserVersion: 'test-1', }, common: { title: { value: '来源商品', confidence: 'verified' }, sourcePrice: { amount: '99.90', currencyCode: 'RUB' }, attributes: [], categories: [], }, variantAxes: [], variants: [ { sourceVariantKey: '123456789', sourceSku: '123456789', displayName: '默认 SKU', selections: [], confidence: 'verified', evidenceRefs: ['page'], }, ], assets: [ { url: 'https://images.example.test/product.jpg', role: 'source_main', relation: 'product_candidate', sortOrder: 0, evidenceRef: 'page', }, ], evidence: [{ ref: 'page', kind: 'page', path: 'test' }], }; const firstKey = randomUUID(); const first = await request('/api/extension/materials', { method: 'POST', headers: { Authorization: `Bearer ${webAccessToken}`, 'Idempotency-Key': firstKey, }, body: JSON.stringify(payload), }); assert.equal(first.response.status, 201); assert.equal(first.body.data.reused, false); assert.equal(first.body.data.collection_item.status, 'ready_for_edit'); assert.equal(first.body.data.materialization.pending_selection, 1); assert.equal(first.body.data.materialization.queued, 0); const editorEntry = new URL(first.body.data.editor_entry_path, 'http://atlas.example.test'); assert.equal(editorEntry.pathname, `/products/${first.body.data.product_id}/editor`); assert.equal(editorEntry.searchParams.get('collection_item_id'), first.body.data.collection_item.id); const replay = await request('/api/extension/materials', { method: 'POST', headers: { Authorization: `Bearer ${webAccessToken}`, 'Idempotency-Key': firstKey, }, body: JSON.stringify(payload), }); assert.deepEqual(replay.body, first.body); const profilesBeforeEntry = await database.workspaceTransaction(workspaceId, (tx) => tx.query('SELECT id FROM atlas.product_platform_profiles WHERE product_id=$1', [first.body.data.product_id]), ); assert.equal(profilesBeforeEntry.rowCount, 0); const source1688 = { source: { platform: '1688', itemKey: '1688-price-image-test', canonicalUrl: 'https://detail.1688.com/offer/123456789.html', collectedAt: new Date().toISOString(), parserVersion: '1688-test', }, common: { attributes: [], categories: [] }, variantAxes: [], variants: [{ sourceVariantKey: '6100454176961', sourceSku: '6100454176961', displayName: '六代目卡卡西25cm', selections: [], sourcePrice: { amount: '31.335', currencyCode: 'CNY' }, confidence: 'verified', evidenceRefs: ['1688:sku-map'], }], assets: [{ url: 'https://cbu01.alicdn.com/sku.jpg', role: 'source_sku', relation: 'axis_value', variantSourceKey: '6100454176961', sortOrder: 0, evidenceRef: '1688:sku-map', }], evidence: [{ ref: '1688:sku-map', kind: 'state', path: 'skuModel.skuInfoMap' }], }; const captured1688 = await request('/api/extension/materials', { method: 'POST', headers: { Authorization: `Bearer ${webAccessToken}`, 'Idempotency-Key': randomUUID(), }, body: JSON.stringify(source1688), }); assert.equal(captured1688.response.status, 201); const associated1688 = await database.workspaceTransaction(workspaceId, async (tx) => tx.query<{ purchase_cost_cny: string; source_variant_id: string; variant_id: string }>( `SELECT v.purchase_cost_cny::text, t.source_variant_id, v.id AS variant_id FROM atlas.product_variants v JOIN atlas.collection_asset_transfers t ON t.product_id = v.product_id WHERE v.product_id = $1 AND t.role = 'source_sku'`, [captured1688.body.data.product_id], ), ); assert.equal(associated1688.rows[0]?.purchase_cost_cny, '31.3400'); assert.equal(associated1688.rows[0]?.source_variant_id, associated1688.rows[0]?.variant_id); const refreshed1688 = await request('/api/extension/materials', { method: 'POST', headers: { Authorization: `Bearer ${webAccessToken}`, 'Idempotency-Key': randomUUID(), }, body: JSON.stringify({ ...source1688, variants: [{ ...source1688.variants[0], sourcePrice: { amount: '32.525', currencyCode: 'CNY' }, }], }), }); assert.equal(refreshed1688.response.status, 200); const updatedCost = await database.workspaceTransaction(workspaceId, async (tx) => tx.query<{ purchase_cost_cny: string }>( `SELECT purchase_cost_cny::text FROM atlas.product_variants WHERE product_id = $1`, [captured1688.body.data.product_id], ), ); assert.equal(updatedCost.rows[0]?.purchase_cost_cny, '32.5300'); const platforms = await request('/api/platforms', { headers: { Authorization: `Bearer ${webAccessToken}` }, }); const targetPlatform = platforms.body.data.find( (platform: { code: string }) => platform.code === 'OZON', ); assert.ok(targetPlatform); await database.workspaceTransaction(workspaceId, async (tx) => { await tx.query( `INSERT INTO atlas.stores (workspace_id, platform_id, name, platform_client_id, currency_code, is_default, status, credential_status) VALUES (current_setting('app.workspace_id')::uuid, $1, '复采测试店铺', $2, 'RUB', true, 'active', 'valid')`, [targetPlatform.id, `refresh-${randomUUID()}`], ); }); const editorContext = await request( `/api/collection-items/${editorEntry.searchParams.get('collection_item_id')}/editor-contexts`, { method: 'POST', headers: { Authorization: `Bearer ${webAccessToken}`, 'X-Atlas-Platform-Id': targetPlatform.id, 'Idempotency-Key': randomUUID(), }, }, ); assert.equal(editorContext.response.status, 201); assert.equal(editorContext.body.data.product_id, first.body.data.product_id); assert.equal(editorContext.body.data.editor_path, editorEntry.pathname); const readEditor = (productId: string) => request( `/api/products/${productId}/editor`, { headers: { Authorization: `Bearer ${webAccessToken}`, 'X-Atlas-Platform-Id': targetPlatform.id, } }, ); const initialEditor = await readEditor(first.body.data.product_id); assert.equal(initialEditor.response.status, 200); assert.equal(initialEditor.body.data.product.common.purchase_url, payload.source.canonicalUrl); const sourceLink = await database.workspaceTransaction(workspaceId, (tx) => tx.query( 'SELECT purchase_url FROM atlas.products WHERE id=$1', [captured1688.body.data.product_id], )); assert.equal(sourceLink.rows[0].purchase_url, source1688.source.canonicalUrl); await request(`/api/collection-items/${captured1688.body.data.collection_item.id}/editor-contexts`, { method: 'POST', headers: { Authorization: `Bearer ${webAccessToken}`, 'X-Atlas-Platform-Id': targetPlatform.id, 'Idempotency-Key': randomUUID(), }, }); // 模拟历史采集商品:原来只有采集记录保存了页面地址,商品货源字段为空。 await database.workspaceTransaction(workspaceId, (tx) => tx.query( 'UPDATE atlas.products SET purchase_url=NULL WHERE id=$1', [captured1688.body.data.product_id], )); const historicalEditor = await readEditor(captured1688.body.data.product_id); assert.equal(historicalEditor.response.status, 200); assert.equal(historicalEditor.body.data.product.common.purchase_url, source1688.source.canonicalUrl); // Simulate real draft work before the source page is collected again. // Re-collection must neither delete this SKU nor overwrite its Atlas // fields / platform mapping just because a newer source snapshot arrives. await database.workspaceTransaction(workspaceId, async (tx) => { const variant = await tx.query<{ id: string }>( `SELECT id FROM atlas.product_variants WHERE product_id = $1 AND source_variant_key = '123456789'`, [first.body.data.product_id], ); await tx.query( `UPDATE atlas.products SET source_title = '人工确认商品名', purchase_url = 'https://detail.1688.com/offer/888888888.html' WHERE id = $1`, [first.body.data.product_id], ); await tx.query( `UPDATE atlas.product_variants SET display_name = '人工确认 SKU', purchase_cost_cny = '88.80' WHERE id = $1`, [variant.rows[0].id], ); await tx.query( `UPDATE atlas.product_variant_platform_listings SET offer_id = 'manual-offer-001' WHERE product_variant_id = $1`, [variant.rows[0].id], ); }); // A sibling Ozon URL has a different entered SKU. It must still reuse // the item because the submitted real-SKU sets overlap at 123456789. const refreshedPayload = { ...payload, source: { ...payload.source, itemKey: '987654321', canonicalUrl: 'https://www.ozon.ru/product/demo-987654321/', collectedAt: new Date(Date.now() + 1_000).toISOString(), parserVersion: 'test-2', }, common: { ...payload.common, title: { value: '第二次采集的中文标题', confidence: 'verified', }, sourcePrice: { amount: '109.90', currencyCode: 'RUB' }, }, variants: [ { sourceVariantKey: '123456789', sourceSku: '123456789', displayName: '第一 SKU', selections: [], confidence: 'verified', evidenceRefs: ['page'], }, { sourceVariantKey: '987654321', sourceSku: '987654321', displayName: '第二次采集 SKU', selections: [], confidence: 'verified', evidenceRefs: ['page'], }, ], assets: [ { url: 'https://images.example.test/refreshed-product.jpg', role: 'source_main', relation: 'product_candidate', sortOrder: 0, evidenceRef: 'page', }, { url: 'https://videos.example.test/refreshed-product.mp4', mediaType: 'video', posterUrl: 'https://images.example.test/refreshed-video-poster.jpg', role: 'source_main', relation: 'product_candidate', sortOrder: 1, evidenceRef: 'page', }, ], }; const second = await request('/api/extension/materials', { method: 'POST', headers: { Authorization: `Bearer ${webAccessToken}`, 'Idempotency-Key': randomUUID(), }, body: JSON.stringify(refreshedPayload), }); assert.equal(second.response.status, 200); assert.equal(second.body.data.reused, true); assert.equal(second.body.data.refreshed, true); assert.equal(second.body.data.product_id, first.body.data.product_id); assert.equal(second.body.data.editor_entry_path, first.body.data.editor_entry_path); assert.equal( second.body.data.collection_item.id, first.body.data.collection_item.id, ); assert.equal(second.body.data.materialization.pending_selection, 2); assert.equal(second.body.data.materialization.queued, 0); const editorAfterRefresh = await request( `/api/products/${first.body.data.product_id}/editor`, { headers: { Authorization: `Bearer ${webAccessToken}`, 'X-Atlas-Platform-Id': targetPlatform.id, }, }, ); assert.equal(editorAfterRefresh.response.status, 200); assert.equal( editorAfterRefresh.body.data.product.common.purchase_url, 'https://detail.1688.com/offer/888888888.html', ); assert.equal( editorAfterRefresh.body.data.product.common.source_title, '人工确认商品名', ); const originalVariant = editorAfterRefresh.body.data.variants.find( (variant: { source_variant_key: string }) => variant.source_variant_key === '123456789', ); assert.equal(originalVariant.display_name, '人工确认 SKU'); assert.equal(originalVariant.purchase_cost_cny, '88.80'); assert.equal(originalVariant.listing.offer_id, 'manual-offer-001'); const discoveredVariant = editorAfterRefresh.body.data.variants.find( (variant: { source_variant_key: string }) => variant.source_variant_key === '987654321', ); assert.ok(discoveredVariant); assert.equal(discoveredVariant.listing.status, 'not_listed'); const inspection = await request( `/api/collection-items/${first.body.data.collection_item.id}`, { headers: { Authorization: `Bearer ${webAccessToken}` } }, ); assert.equal(inspection.response.status, 200); assert.equal(inspection.body.data.title, '第二次采集的中文标题'); assert.equal( inspection.body.data.normalized_payload.source.parserVersion, 'test-2', ); assert.equal(inspection.body.data.variants.length, 2); assert.deepEqual( inspection.body.data.variants.map( (variant: { source_variant_key: string }) => variant.source_variant_key, ), ['123456789', '987654321'], ); assert.equal(inspection.body.data.asset_transfers.length, 2); assert.equal( inspection.body.data.asset_transfers[0].source_url, 'https://images.example.test/refreshed-product.jpg', ); assert.equal( inspection.body.data.asset_transfers[0].status, 'pending_selection', ); const videoTransfer = inspection.body.data.asset_transfers.find( (transfer: { media_type: string }) => transfer.media_type === 'video', ); assert.equal( videoTransfer?.source_url, 'https://videos.example.test/refreshed-product.mp4', ); assert.equal( videoTransfer?.poster_url, 'https://images.example.test/refreshed-video-poster.jpg', ); const eventsBeforeSelection = await database.workspaceTransaction( workspaceId, async (tx) => tx.query<{ count: string }>( `SELECT count(*)::text AS count FROM atlas.outbox_events WHERE event_type = 'collection.assets.materialize.requested' AND event_payload->>'collection_item_id' = $1`, [first.body.data.collection_item.id], ), ); assert.equal(eventsBeforeSelection.rows[0]?.count, '0'); const materialization = await request( `/api/collection-items/${first.body.data.collection_item.id}/asset-materializations`, { method: 'POST', headers: { Authorization: `Bearer ${webAccessToken}`, 'Idempotency-Key': randomUUID(), }, body: JSON.stringify({ transfer_ids: [inspection.body.data.asset_transfers[0].id], }), }, ); assert.equal(materialization.response.status, 202); const afterSelection = await request( `/api/collection-items/${first.body.data.collection_item.id}`, { headers: { Authorization: `Bearer ${webAccessToken}` } }, ); assert.equal(afterSelection.response.status, 200); assert.ok( ['queued', 'running', 'failed', 'succeeded'].includes( afterSelection.body.data.asset_transfers[0].status, ), ); const eventsAfterSelection = await database.workspaceTransaction( workspaceId, async (tx) => tx.query<{ count: string }>( `SELECT count(*)::text AS count FROM atlas.outbox_events WHERE event_type = 'collection.assets.materialize.requested' AND event_payload->>'collection_item_id' = $1`, [first.body.data.collection_item.id], ), ); assert.equal(eventsAfterSelection.rows[0]?.count, '1'); const transferWorkspace = await database.workspaceTransaction( workspaceId, async (tx) => tx.query<{ workspace_id: string }>( 'SELECT workspace_id FROM atlas.collection_asset_transfers WHERE id = $1', [inspection.body.data.asset_transfers[0].id], ), ); assert.equal(transferWorkspace.rows[0]?.workspace_id, workspaceId); }); it('完整采集并保存 1000 个 SKU,复采保持身份,超限请求原子拒绝', async () => { const variants = Array.from({ length: PRODUCT_VARIANT_LIMIT }, (_, index) => ({ sourceVariantKey: String(8_000_000_000_000 + index), sourceSku: String(8_000_000_000_000 + index), displayName: `来源 SKU ${index}`, selections: [], confidence: 'verified', evidenceRefs: [], sourcePrice: { amount: '20.00', currencyCode: 'CNY' }, })); const payload = { source: { platform: '1688', itemKey: `large-${randomUUID()}`, canonicalUrl: 'https://detail.1688.com/offer/123456789.html', collectedAt: new Date().toISOString(), parserVersion: 'large-sku-test', }, common: { title: { value: '多 SKU 来源商品', confidence: 'verified' }, attributes: [], categories: [] }, variantAxes: [], variants, assets: [], evidence: [], }; const capture = (body: unknown) => request('/api/extension/materials', { method: 'POST', headers: { Authorization: `Bearer ${webAccessToken}`, 'Idempotency-Key': randomUUID() }, body: JSON.stringify(body), }); const captured = await capture(payload); assert.equal(captured.response.status, 201, JSON.stringify(captured.body)); assert.equal(captured.body.data.collection_item.variant_count, PRODUCT_VARIANT_LIMIT); const collectionId = captured.body.data.collection_item.id; const productId = captured.body.data.product_id; const platforms = await request('/api/platforms', { headers: { Authorization: `Bearer ${webAccessToken}` } }); const platformId = platforms.body.data.find((platform: { code: string }) => platform.code === 'OZON').id; const context = await request(`/api/collection-items/${collectionId}/editor-contexts`, { method: 'POST', headers: { Authorization: `Bearer ${webAccessToken}`, 'X-Atlas-Platform-Id': platformId, 'Idempotency-Key': randomUUID(), }, }); assert.equal(context.response.status, 201, JSON.stringify(context.body)); const read = () => request(`/api/products/${productId}/editor`, { headers: { Authorization: `Bearer ${webAccessToken}`, 'X-Atlas-Platform-Id': platformId }, }); const before = await read(); assert.equal(before.response.status, 200); const snapshot = before.body.data; assert.equal(snapshot.variants.length, PRODUCT_VARIANT_LIMIT); assert.equal(snapshot.variants.at(-1).source_sku, variants.at(-1)!.sourceSku); const draft = { ai_source_material: snapshot.source_material, ai_source_overridden_fields: [], common: { ...snapshot.product.common, default_physical_spec: snapshot.product.default_physical_spec, default_package_spec: snapshot.product.default_package_spec, }, profile: { target_store_id: snapshot.profile.target_store.id, fulfillment: snapshot.profile.fulfillment, title: null, description: null, tags: [], platform_product_category_id: null, }, variants: snapshot.variants.map((variant: any, index: number) => ({ id: variant.id, client_key: null, display_name: index === PRODUCT_VARIANT_LIMIT - 1 ? '末尾 SKU 已编辑' : variant.display_name, source_sku: variant.source_sku, specification_values: variant.specification_values, purchase_cost_cny: variant.purchase_cost_cny, physical_spec: variant.physical_spec, package_spec: variant.package_spec, tags: [], description: null, offer_id: null, media_asset_ids: [], })), }; const saved = await request(`/api/products/${productId}/editor-draft`, { method: 'PUT', headers: { Authorization: `Bearer ${webAccessToken}`, 'X-Atlas-Platform-Id': platformId, 'Idempotency-Key': randomUUID(), 'If-Match': `"${snapshot.product.revision}:${snapshot.profile.revision}"`, }, body: JSON.stringify(draft), }); assert.equal(saved.response.status, 200, JSON.stringify(saved.body)); const after = await read(); assert.equal(after.body.data.variants.length, PRODUCT_VARIANT_LIMIT); assert.equal(after.body.data.variants.at(-1).display_name, '末尾 SKU 已编辑'); const refreshed = await capture(payload); assert.equal(refreshed.response.status, 200); assert.equal(refreshed.body.data.product_id, productId); assert.equal(refreshed.body.data.collection_item.variant_count, PRODUCT_VARIANT_LIMIT); const reread = await read(); assert.deepEqual(reread.body.data.variants.map((v: any) => v.id), snapshot.variants.map((v: any) => v.id)); assert.equal(reread.body.data.variants.at(-1).display_name, '末尾 SKU 已编辑'); const revision = reread.body.data.source_material.source_snapshot_revision; const rejected = await capture({ ...payload, variants: [...variants, { ...variants[0], sourceVariantKey: 'extra-sku' }] }); assert.equal(rejected.response.status, 422); assert.equal(rejected.body.error.details[0].field, 'variants'); const unchanged = await read(); assert.equal(unchanged.body.data.source_material.source_snapshot_revision, revision); assert.equal(unchanged.body.data.variants.length, PRODUCT_VARIANT_LIMIT); }, 60_000); });