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: string; let token = ''; let workspaceId = ''; let userId = ''; let platformId = ''; let storeId = ''; let listingId = ''; let runId = ''; async function request(path: string, init: RequestInit = {}) { const response = await fetch(`${baseUrl}${path}`, { ...init, headers: { 'Content-Type': 'application/json', ...init.headers }, }); return { response, body: (await response.json()) as Record }; } function auth(extra: Record = {}) { return { Authorization: `Bearer ${token}`, 'X-Atlas-Platform-Id': platformId, ...extra, }; } 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}`; const registered = await request('/api/auth/register', { method: 'POST', body: JSON.stringify({ email: `online-${randomUUID()}@example.test`, password: 'correct-horse-battery-staple-123', password_confirmation: 'correct-horse-battery-staple-123', }), }); assert.equal(registered.response.status, 201); token = registered.body.data.access_token; workspaceId = registered.body.data.workspace.id; userId = registered.body.data.user.id; const platforms = await request('/api/platforms', { headers: { Authorization: `Bearer ${token}` }, }); platformId = platforms.body.data.find( (platform: { code: string }) => platform.code === 'OZON', ).id; storeId = randomUUID(); await database.workspaceTransaction(workspaceId, async (tx) => { await tx.query( `INSERT INTO atlas.stores (id, workspace_id, platform_id, name, platform_client_id, currency_code, is_default, status, credential_status) VALUES ($1, $2, $3, '测试门店', $4, 'RUB', true, 'active', 'valid')`, [storeId, workspaceId, platformId, `online-http-${storeId}`], ); const run = await tx.query<{ id: string }>( `INSERT INTO atlas.platform_product_sync_runs (workspace_id, store_id, platform_id, trigger_source, status, completed_at) VALUES ($1, $2, $3, 'manual', 'succeeded', now()) RETURNING id`, [workspaceId, storeId, platformId], ); runId = run.rows[0].id; const listing = await tx.query<{ id: string }>( `INSERT INTO atlas.platform_listings (workspace_id, store_id, platform_id, remote_product_id, offer_id, remote_sku, name, currency_code, selling_price, visible, moderation_status, stock_summary, image_summary, source, last_sync_run_id, last_synced_at, raw_payload) VALUES ($1, $2, $3, '123456', 'test-offer', '654321', '测试商品', 'RUB', 99, true, 'approved', '{"total":0,"has_data":true}', '{"url":null,"width_px":null,"height_px":null}', 'external_sync', $4, now(), '{}') RETURNING id`, [workspaceId, storeId, platformId, runId], ); listingId = listing.rows[0].id; await tx.query( `INSERT INTO atlas.platform_listings (workspace_id, store_id, platform_id, remote_product_id, offer_id, name, visible, stock_summary, image_summary, source, last_sync_run_id, last_synced_at, missing_since, archived_at, raw_payload) VALUES ($1, $2, $3, '123457', 'archived-offer', '归档商品', NULL, '{"total":0,"has_data":false}', '{}', 'external_sync', $4, now(), now() - interval '8 days', now(), '{}')`, [workspaceId, storeId, platformId, runId], ); }); }); afterAll(async () => { if (workspaceId && database) { await database.workspaceTransaction(workspaceId, async (tx) => { await tx.query( 'DELETE FROM atlas.platform_listing_sync_snapshots WHERE workspace_id = $1', [workspaceId], ); await tx.query( 'DELETE FROM atlas.platform_listings WHERE workspace_id = $1', [workspaceId], ); await tx.query( 'DELETE FROM atlas.platform_product_sync_runs WHERE workspace_id = $1', [workspaceId], ); 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.store_secrets 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, ]); } if (app) await app.close(); }); it('列表、详情与同步历史按平台店铺返回镜像,不返回原始响应', async () => { const listed = await request( `/api/platform-listings?store_id=${storeId}&q=test-offer`, { headers: auth() }, ); assert.equal(listed.response.status, 200); assert.equal(listed.body.data.length, 1); assert.equal(listed.body.data[0].visible, true); assert.equal(listed.body.data[0].selling_price, '99.00'); assert.deepEqual(listed.body.data[0].stock_summary, { total: 0, has_data: true, }); assert.equal(listed.body.meta.store.id, storeId); assert.equal(listed.body.meta.last_sync_status, 'succeeded'); assert.equal('raw_payload' in listed.body.data[0], false); const detail = await request(`/api/platform-listings/${listingId}`, { headers: auth(), }); assert.equal(detail.response.status, 200); assert.equal(detail.body.data.offer_id, 'test-offer'); const runs = await request( `/api/stores/${storeId}/platform-product-sync-runs`, { headers: auth() }, ); assert.equal(runs.response.status, 200); assert.equal(runs.body.data[0].id, runId); const run = await request(`/api/platform-product-sync-runs/${runId}`, { headers: auth(), }); assert.equal(run.response.status, 200); assert.equal(run.body.data.status, 'succeeded'); }); it('筛选未知可见性与缺少平台头的请求按契约处理', async () => { const unknown = await request( `/api/platform-listings?store_id=${storeId}&visibility=unknown`, { headers: auth() }, ); assert.equal(unknown.response.status, 200); assert.equal(unknown.body.data.length, 0); const archived = await request( `/api/platform-listings?store_id=${storeId}&visibility=unknown&include_archived=true`, { headers: auth() }, ); assert.equal(archived.response.status, 200); assert.equal(archived.body.data.length, 1); assert.equal(archived.body.data[0].offer_id, 'archived-offer'); const missingHeader = await request('/api/platform-listings', { headers: { Authorization: `Bearer ${token}` }, }); assert.equal(missingHeader.response.status, 400); assert.equal( missingHeader.body.error.code, 'PLATFORM_CONTEXT_REQUIRED', ); const noCredentials = await request( `/api/stores/${storeId}/platform-product-sync-runs`, { method: 'POST', headers: auth({ 'Idempotency-Key': randomUUID() }), body: JSON.stringify({ reason: 'manual_refresh' }), }, ); assert.equal(noCredentials.response.status, 422); assert.equal( noCredentials.body.error.code, 'STORE_CREDENTIAL_UNAVAILABLE', ); }); it('触发同步写入 Outbox,同一幂等键复用任务,并发请求返回冲突', async () => { await database.workspaceTransaction(workspaceId, (tx) => tx.query( `INSERT INTO atlas.store_secrets (workspace_id, store_id, kind, secret_ciphertext, secret_nonce, encryption_key_version, fingerprint, status) VALUES ($1, $2, 'seller_api_key', $3, $4, 'v1', $5, 'valid')`, [workspaceId, storeId, Buffer.alloc(32), Buffer.alloc(12), randomUUID()], ), ); const key = randomUUID(); const post = (idempotencyKey: string) => request(`/api/stores/${storeId}/platform-product-sync-runs`, { method: 'POST', headers: auth({ 'Idempotency-Key': idempotencyKey }), body: JSON.stringify({ reason: 'manual_refresh' }), }); const created = await post(key); assert.equal(created.response.status, 202); assert.equal(created.body.data.sync_run.status, 'queued'); const repeated = await post(key); assert.equal(repeated.response.status, 202); assert.equal(repeated.body.data.sync_run.id, created.body.data.sync_run.id); const concurrent = await post(randomUUID()); assert.equal(concurrent.response.status, 409); assert.equal(concurrent.body.error.code, 'SYNC_ALREADY_RUNNING'); const events = await database.workspaceTransaction(workspaceId, (tx) => tx.query<{ count: string }>( `SELECT count(*)::text AS count FROM atlas.outbox_events WHERE aggregate_id = $1 AND event_type = 'platform_product_sync.requested'`, [created.body.data.sync_run.id], ), ); assert.equal(events.rows[0].count, '1'); }); });