Files
atlas-seller-kit/apps/api/test/product-editor.e2e.test.ts
T
2026-09-09 15:11:58 +08:00

292 lines
14 KiB
TypeScript

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<string, string> = {}) {
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>(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);
});
});