feat(ai-image): AI生图开发
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
createCipheriv,
|
||||
createHash,
|
||||
randomBytes,
|
||||
randomUUID,
|
||||
} from 'node:crypto';
|
||||
import { test } from 'node:test';
|
||||
import { createApplicationDatabase } from '@atlas/database';
|
||||
import {
|
||||
archiveExpiredMissingOnlineProducts,
|
||||
markOnlineProductSyncFailed,
|
||||
runOnlineProductSync,
|
||||
} from '../src/online-product-sync.ts';
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
const encryptionKey = process.env.STORE_SECRET_ENCRYPTION_KEY;
|
||||
|
||||
test(
|
||||
'全量同步写入镜像,连续未见归档,再出现恢复且租户隔离',
|
||||
{ skip: !databaseUrl || !encryptionKey },
|
||||
async () => {
|
||||
const database = createApplicationDatabase(databaseUrl);
|
||||
const workspaceId = randomUUID();
|
||||
const storeId = randomUUID();
|
||||
const secretId = randomUUID();
|
||||
const platform = await database.query(
|
||||
"SELECT id FROM atlas.platforms WHERE code = 'OZON' LIMIT 1",
|
||||
);
|
||||
assert.ok(platform.rows[0]);
|
||||
const platformId = platform.rows[0].id;
|
||||
const originalFetch = globalThis.fetch;
|
||||
let productPresent = true;
|
||||
let failList = false;
|
||||
let remoteProductId = 93847265;
|
||||
let failAfterFirstPage = false;
|
||||
globalThis.fetch = async (url, init) => {
|
||||
if (String(url).endsWith('/v3/product/list')) {
|
||||
if (failList) return new Response('{}', { status: 503 });
|
||||
const cursor = JSON.parse(init.body).last_id;
|
||||
if (failAfterFirstPage && cursor)
|
||||
return new Response('{}', { status: 503 });
|
||||
return Response.json({
|
||||
result: {
|
||||
items: productPresent
|
||||
? [
|
||||
{
|
||||
product_id: remoteProductId,
|
||||
offer_id: 'sync-test-offer',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
last_id: failAfterFirstPage ? 'second-page' : '',
|
||||
},
|
||||
});
|
||||
}
|
||||
if (String(url).endsWith('/v3/product/info/list'))
|
||||
return Response.json({
|
||||
result: {
|
||||
items: [
|
||||
{
|
||||
id: remoteProductId,
|
||||
offer_id: 'sync-test-offer',
|
||||
name: '同步测试商品',
|
||||
price: '99.00',
|
||||
old_price: '109.00',
|
||||
visible: true,
|
||||
stocks: { present: 0 },
|
||||
sources: [{ sku: 987654 }],
|
||||
status: {
|
||||
moderate_status: 'approved',
|
||||
validation_state: 'success',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
throw new Error('Unexpected Ozon path');
|
||||
};
|
||||
const key = createHash('sha256').update(encryptionKey).digest();
|
||||
const nonce = randomBytes(12);
|
||||
const cipher = createCipheriv('aes-256-gcm', key, nonce);
|
||||
const ciphertext = Buffer.concat([
|
||||
cipher.update('test-key', 'utf8'),
|
||||
cipher.final(),
|
||||
cipher.getAuthTag(),
|
||||
]);
|
||||
try {
|
||||
await database.query(
|
||||
'INSERT INTO atlas.workspaces (id, name, slug) VALUES ($1, $2, $3)',
|
||||
[workspaceId, '在线商品同步测试', `online-sync-${workspaceId}`],
|
||||
);
|
||||
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-sync-${storeId}`,
|
||||
],
|
||||
);
|
||||
await tx.query(
|
||||
`INSERT INTO atlas.store_secrets (id, workspace_id, store_id, kind, secret_ciphertext, secret_nonce, encryption_key_version, fingerprint, status)
|
||||
VALUES ($1, $2, $3, 'seller_api_key', $4, $5, 'v1', $6, 'valid')`,
|
||||
[
|
||||
secretId,
|
||||
workspaceId,
|
||||
storeId,
|
||||
ciphertext,
|
||||
nonce,
|
||||
randomUUID(),
|
||||
],
|
||||
);
|
||||
});
|
||||
const createJob = async () => {
|
||||
const created = await database.workspaceTransaction(
|
||||
workspaceId,
|
||||
(tx) =>
|
||||
tx.query(
|
||||
`INSERT INTO atlas.platform_product_sync_runs
|
||||
(workspace_id, store_id, platform_id, trigger_source) VALUES ($1, $2, $3, 'manual') RETURNING id`,
|
||||
[workspaceId, storeId, platformId],
|
||||
),
|
||||
);
|
||||
const runId = created.rows[0].id;
|
||||
const job = {
|
||||
eventId: randomUUID(),
|
||||
workspaceId,
|
||||
eventType: 'platform_product_sync.requested',
|
||||
payload: {
|
||||
sync_run_id: runId,
|
||||
store_id: storeId,
|
||||
platform_id: platformId,
|
||||
secret_id: secretId,
|
||||
},
|
||||
};
|
||||
return { runId, job };
|
||||
};
|
||||
const sync = async () => {
|
||||
const { runId, job } = await createJob();
|
||||
if (failList) {
|
||||
let failure;
|
||||
try {
|
||||
await runOnlineProductSync(database, job);
|
||||
} catch (error) {
|
||||
failure = error;
|
||||
}
|
||||
assert.ok(failure);
|
||||
await markOnlineProductSyncFailed(database, job, failure);
|
||||
await runOnlineProductSync(database, job);
|
||||
} else await runOnlineProductSync(database, job);
|
||||
const run = await database.workspaceTransaction(
|
||||
workspaceId,
|
||||
(tx) =>
|
||||
tx.query(
|
||||
'SELECT status, listed_count, upserted_count FROM atlas.platform_product_sync_runs WHERE id = $1',
|
||||
[runId],
|
||||
),
|
||||
);
|
||||
assert.equal(
|
||||
run.rows[0].status,
|
||||
failList ? 'failed' : 'succeeded',
|
||||
);
|
||||
return run.rows[0];
|
||||
};
|
||||
const first = await sync();
|
||||
assert.equal(first.listed_count, 1);
|
||||
assert.equal(first.upserted_count, 1);
|
||||
const listing = async () =>
|
||||
database.workspaceTransaction(workspaceId, (tx) =>
|
||||
tx.query(
|
||||
'SELECT id, remote_product_id, name, visible, stock_summary, missing_since, archived_at FROM atlas.platform_listings WHERE store_id = $1',
|
||||
[storeId],
|
||||
),
|
||||
);
|
||||
let row = (await listing()).rows[0];
|
||||
assert.equal(row.name, '同步测试商品');
|
||||
assert.equal(row.visible, true);
|
||||
assert.deepEqual(row.stock_summary, { total: 0, has_data: true });
|
||||
const listingId = row.id;
|
||||
remoteProductId = 93847266;
|
||||
await sync();
|
||||
row = (await listing()).rows[0];
|
||||
assert.equal(row.id, listingId);
|
||||
assert.equal(row.remote_product_id, String(remoteProductId));
|
||||
assert.equal((await listing()).rows.length, 1);
|
||||
failList = true;
|
||||
await sync();
|
||||
row = (await listing()).rows[0];
|
||||
assert.equal(row.missing_since, null);
|
||||
assert.equal(row.archived_at, null);
|
||||
failList = false;
|
||||
productPresent = false;
|
||||
await sync();
|
||||
row = (await listing()).rows[0];
|
||||
assert.ok(row.missing_since);
|
||||
assert.equal(row.archived_at, null);
|
||||
await database.workspaceTransaction(workspaceId, (tx) =>
|
||||
tx.query(
|
||||
`UPDATE atlas.platform_listings SET missing_since = now() - interval '8 days' WHERE store_id = $1`,
|
||||
[storeId],
|
||||
),
|
||||
);
|
||||
const activeRun = await database.workspaceTransaction(
|
||||
workspaceId,
|
||||
(tx) =>
|
||||
tx.query(
|
||||
`INSERT INTO atlas.platform_product_sync_runs (workspace_id, store_id, platform_id, trigger_source)
|
||||
VALUES ($1, $2, $3, 'manual') RETURNING id`,
|
||||
[workspaceId, storeId, platformId],
|
||||
),
|
||||
);
|
||||
await archiveExpiredMissingOnlineProducts(database, workspaceId);
|
||||
assert.equal((await listing()).rows[0].archived_at, null);
|
||||
await database.workspaceTransaction(workspaceId, (tx) =>
|
||||
tx.query(
|
||||
`DELETE FROM atlas.platform_product_sync_runs WHERE id = $1`,
|
||||
[activeRun.rows[0].id],
|
||||
),
|
||||
);
|
||||
await archiveExpiredMissingOnlineProducts(database, workspaceId);
|
||||
row = (await listing()).rows[0];
|
||||
assert.ok(row.archived_at);
|
||||
productPresent = true;
|
||||
await sync();
|
||||
row = (await listing()).rows[0];
|
||||
assert.equal(row.missing_since, null);
|
||||
assert.equal(row.archived_at, null);
|
||||
productPresent = false;
|
||||
await sync();
|
||||
row = (await listing()).rows[0];
|
||||
assert.ok(row.missing_since);
|
||||
assert.equal(row.archived_at, null);
|
||||
await sync();
|
||||
row = (await listing()).rows[0];
|
||||
assert.ok(row.archived_at);
|
||||
productPresent = true;
|
||||
await sync();
|
||||
row = (await listing()).rows[0];
|
||||
assert.equal(row.missing_since, null);
|
||||
assert.equal(row.archived_at, null);
|
||||
productPresent = false;
|
||||
await sync();
|
||||
const missingSinceBeforeRetry = (await listing()).rows[0].missing_since;
|
||||
assert.ok(missingSinceBeforeRetry);
|
||||
productPresent = true;
|
||||
const { job: retryJob } = await createJob();
|
||||
failAfterFirstPage = true;
|
||||
await assert.rejects(runOnlineProductSync(database, retryJob));
|
||||
row = (await listing()).rows[0];
|
||||
assert.equal(row.missing_since.toISOString(), missingSinceBeforeRetry.toISOString());
|
||||
assert.equal(row.archived_at, null);
|
||||
failAfterFirstPage = false;
|
||||
productPresent = false;
|
||||
await runOnlineProductSync(database, retryJob);
|
||||
row = (await listing()).rows[0];
|
||||
assert.ok(row.missing_since);
|
||||
assert.ok(row.archived_at);
|
||||
const otherWorkspace = await database.workspaceTransaction(
|
||||
randomUUID(),
|
||||
(tx) =>
|
||||
tx.query(
|
||||
'SELECT id FROM atlas.platform_listings WHERE store_id = $1',
|
||||
[storeId],
|
||||
),
|
||||
);
|
||||
assert.equal(otherWorkspace.rows.length, 0);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
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.store_secrets WHERE workspace_id = $1',
|
||||
[workspaceId],
|
||||
);
|
||||
await tx.query(
|
||||
'DELETE FROM atlas.stores WHERE workspace_id = $1',
|
||||
[workspaceId],
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
await database
|
||||
.query('DELETE FROM atlas.workspaces WHERE id = $1', [
|
||||
workspaceId,
|
||||
])
|
||||
.catch(() => {});
|
||||
await database.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user