Update seller store experience
This commit is contained in:
@@ -7,30 +7,326 @@ 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 = '';
|
||||
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 }; }
|
||||
let app: NestExpressApplication;
|
||||
let database: Database;
|
||||
let baseUrl = '';
|
||||
let workspaceId = '';
|
||||
let userId = '';
|
||||
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>(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.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.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(); });
|
||||
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}`;
|
||||
});
|
||||
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_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 登录已过期,请重新登录');
|
||||
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; const 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 first = await request('/api/extension/materials', { method: 'POST', headers: { Authorization: `Bearer ${webAccessToken}`, 'Idempotency-Key': randomUUID() }, body: JSON.stringify(payload) }); assert.equal(first.response.status, 201); assert.equal(first.body.data.reused, false); assert.equal(first.body.data.materialization.queued, 1);
|
||||
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;
|
||||
const 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 first = await request('/api/extension/materials', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${webAccessToken}`,
|
||||
'Idempotency-Key': randomUUID(),
|
||||
},
|
||||
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);
|
||||
// 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' }] };
|
||||
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.collection_item.id, first.body.data.collection_item.id); assert.equal(second.body.data.materialization.queued, 1);
|
||||
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, 1); assert.equal(inspection.body.data.asset_transfers[0].source_url, 'https://images.example.test/refreshed-product.jpg');
|
||||
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.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 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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user