Files
atlas-seller-kit/apps/worker/test/online-product-sync.integration.test.mjs
T

305 lines
13 KiB
JavaScript

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