feat(ai-image): AI生图开发
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
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<string, any> };
|
||||
}
|
||||
|
||||
function auth(extra: Record<string, string> = {}) {
|
||||
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>(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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user