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'; let app: NestExpressApplication; let database: Database; let baseUrl = ''; let workspaceId = ''; let userId = ''; let token = ''; let platformId = ''; let first: { collectionItemId: string; productId: string; profileId: string }; async function request(path: string, init: RequestInit = {}) { const response = await fetch(`${baseUrl}${path}`, { ...init, headers: { 'Content-Type': 'application/json', ...init.headers }, }); return { response, body: response.status === 204 ? undefined : ((await response.json()) as any), }; } function headers(extra: Record = {}) { return { Authorization: `Bearer ${token}`, ...extra }; } async function createCollectionItem(title: string) { const created = await request('/api/collection-items', { method: 'POST', headers: headers({ 'Idempotency-Key': randomUUID() }), body: JSON.stringify({ title }), }); assert.equal(created.response.status, 201); return { collectionItemId: created.body.data.collection_item.id as string, productId: created.body.data.product_id as string, }; } describe('商品编辑 M3.0 / M3.1 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}`; const registration = await request('/api/auth/register', { method: 'POST', body: JSON.stringify({ email: `editor-${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; token = registration.body.data.access_token; const platforms = await request('/api/platforms', { headers: headers() }); platformId = platforms.body.data[0].id; }); 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.product_unmapped_attributes WHERE workspace_id = $1', [workspaceId]); await tx.query('DELETE FROM atlas.product_attributes 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.collection_asset_transfers WHERE workspace_id = $1', [workspaceId]); await tx.query('DELETE FROM atlas.product_variants 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.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.stores 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 created = await createCollectionItem('编辑器测试商品 A'); const missingStore = await request(`/api/collection-items/${created.collectionItemId}/editor-contexts`, { method: 'POST', headers: headers({ 'X-Atlas-Platform-Id': platformId, 'Idempotency-Key': randomUUID() }), }); assert.equal(missingStore.response.status, 422); assert.equal(missingStore.body.error.code, 'DEFAULT_STORE_REQUIRED'); 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, 'M3 测试店铺', $2, 'RUB', true, 'active', 'valid')`, [platformId, `editor-${randomUUID()}`], ); }); const entered = await request(`/api/collection-items/${created.collectionItemId}/editor-contexts`, { method: 'POST', headers: headers({ 'X-Atlas-Platform-Id': platformId, 'Idempotency-Key': randomUUID() }), }); assert.equal(entered.response.status, 201); first = { collectionItemId: created.collectionItemId, productId: created.productId, profileId: entered.body.data.profile_id, }; const enteredAgain = await request(`/api/collection-items/${created.collectionItemId}/editor-contexts`, { method: 'POST', headers: headers({ 'X-Atlas-Platform-Id': platformId, 'Idempotency-Key': randomUUID() }), }); assert.equal(enteredAgain.response.status, 200); assert.equal(enteredAgain.body.data.profile_id, first.profileId); const editor = await request(`/api/products/${first.productId}/editor`, { headers: headers({ 'X-Atlas-Platform-Id': platformId }), }); assert.equal(editor.response.status, 200); assert.equal(editor.body.data.product.id, first.productId); assert.equal(editor.body.data.profile.id, first.profileId); assert.equal(editor.body.data.variants.length, 1); assert.equal(editor.body.data.variants[0].listing.status, 'not_listed'); }); it('数据库拒绝跨商品 SKU 与 profile 的关联,并保留未映射属性草稿', async () => { const second = await createCollectionItem('编辑器测试商品 B'); const secondContext = await request(`/api/collection-items/${second.collectionItemId}/editor-contexts`, { method: 'POST', headers: headers({ 'X-Atlas-Platform-Id': platformId, 'Idempotency-Key': randomUUID() }), }); assert.equal(secondContext.response.status, 201); const link = await database.workspaceTransaction(workspaceId, async (tx) => { const secondVariant = await tx.query<{ id: string }>( 'SELECT id FROM atlas.product_variants WHERE product_id = $1 AND deleted_at IS NULL', [second.productId], ); const firstProfile = await tx.query<{ target_store_id: string }>( 'SELECT target_store_id FROM atlas.product_platform_profiles WHERE id = $1', [first.profileId], ); return { variantId: secondVariant.rows[0].id, storeId: firstProfile.rows[0].target_store_id, }; }); await assert.rejects( database.workspaceTransaction(workspaceId, async (tx) => tx.query( `INSERT INTO atlas.product_variant_platform_listings (workspace_id, product_id, profile_id, platform_id, product_variant_id, store_id) VALUES (current_setting('app.workspace_id')::uuid, $1, $2, $3, $4, $5)`, [second.productId, first.profileId, platformId, link.variantId, link.storeId], ), ), { code: '23503' }, ); await database.workspaceTransaction(workspaceId, async (tx) => { await tx.query( `INSERT INTO atlas.product_unmapped_attributes (workspace_id, product_id, profile_id, platform_id, original_platform_attribute_id, original_name, values, reason) VALUES (current_setting('app.workspace_id')::uuid, $1, $2, $3, 'legacy-material', '旧类目材质', '["棉"]'::jsonb, 'category_changed')`, [first.productId, first.profileId, platformId], ); }); const editor = await request(`/api/products/${first.productId}/editor`, { headers: headers({ 'X-Atlas-Platform-Id': platformId }), }); assert.equal(editor.response.status, 200); assert.equal(editor.body.data.unmapped_attributes.length, 1); assert.equal(editor.body.data.unmapped_attributes[0].original_name, '旧类目材质'); assert.equal(editor.body.data.warnings[0].code, 'UNMAPPED_ATTRIBUTES_PENDING'); }); it('原子保存草稿会更新双版本、创建 SKU,并拒绝过期版本覆盖', async () => { const before = await request(`/api/products/${first.productId}/editor`, { headers: headers({ 'X-Atlas-Platform-Id': platformId }), }); const existing = before.body.data.variants[0]; const payload = { common: { source_title: '已由编辑器确认的商品名', brand: 'Atlas', model: null, barcode: null, purchase_url: null, supply_note: 'M3.1 集成测试', }, profile: { target_store_id: before.body.data.profile.target_store.id, fulfillment: 'rfbs', title: 'Ozon 测试标题', description: null, tags: ['测试', '草稿'], }, variants: [ { id: existing.id, client_key: null, copy_from_variant_id: null, display_name: '默认 SKU(已编辑)', source_sku: existing.source_sku, purchase_cost_cny: '88.50', offer_id: 'm3-variant-a', }, { id: null, client_key: 'new-variant-b', copy_from_variant_id: existing.id, display_name: '新增 SKU', source_sku: null, purchase_cost_cny: '99.00', offer_id: 'm3-variant-b', }, ], }; const key = randomUUID(); const saved = await request(`/api/products/${first.productId}/editor-draft`, { method: 'PUT', headers: headers({ 'X-Atlas-Platform-Id': platformId, 'Idempotency-Key': key, 'If-Match': `"${before.body.data.product.revision}:${before.body.data.profile.revision}"`, }), body: JSON.stringify(payload), }); assert.equal(saved.response.status, 200); assert.equal(saved.body.data.created_variants.length, 1); assert.equal(saved.body.data.product_revision, before.body.data.product.revision + 1); assert.equal(saved.body.data.profile_revision, before.body.data.profile.revision + 1); const replay = await request(`/api/products/${first.productId}/editor-draft`, { method: 'PUT', headers: headers({ 'X-Atlas-Platform-Id': platformId, 'Idempotency-Key': key, 'If-Match': `"${before.body.data.product.revision}:${before.body.data.profile.revision}"`, }), body: JSON.stringify(payload), }); assert.deepEqual(replay.body, saved.body); const after = await request(`/api/products/${first.productId}/editor`, { headers: headers({ 'X-Atlas-Platform-Id': platformId }), }); assert.equal(after.body.data.product.common.source_title, '已由编辑器确认的商品名'); assert.equal(after.body.data.profile.title, 'Ozon 测试标题'); assert.equal(after.body.data.variants.length, 2); assert.equal(after.body.data.variants[0].listing.offer_id, 'm3-variant-a'); const stale = await request(`/api/products/${first.productId}/editor-draft`, { method: 'PUT', headers: headers({ 'X-Atlas-Platform-Id': platformId, 'Idempotency-Key': randomUUID(), 'If-Match': `"${before.body.data.product.revision}:${before.body.data.profile.revision}"`, }), body: JSON.stringify(payload), }); assert.equal(stale.response.status, 409); assert.equal(stale.body.error.code, 'VERSION_CONFLICT'); const removeSecond = { ...payload, variants: [payload.variants[0]] }; const removed = await request(`/api/products/${first.productId}/editor-draft`, { method: 'PUT', headers: headers({ 'X-Atlas-Platform-Id': platformId, 'Idempotency-Key': randomUUID(), 'If-Match': `"${after.body.data.product.revision}:${after.body.data.profile.revision}"`, }), body: JSON.stringify(removeSecond), }); assert.equal(removed.response.status, 200); const replacement = { ...removeSecond, variants: [ removeSecond.variants[0], { id: null, client_key: 'replacement-variant-b', copy_from_variant_id: existing.id, display_name: '替代 SKU', source_sku: null, purchase_cost_cny: '99.00', offer_id: 'm3-variant-b', }, ], }; const reusedOffer = await request(`/api/products/${first.productId}/editor-draft`, { method: 'PUT', headers: headers({ 'X-Atlas-Platform-Id': platformId, 'Idempotency-Key': randomUUID(), 'If-Match': `"${removed.body.data.product_revision}:${removed.body.data.profile_revision}"`, }), body: JSON.stringify(replacement), }); assert.equal(reusedOffer.response.status, 200); }); });