87 lines
3.1 KiB
JavaScript
87 lines
3.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { existsSync, readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
const root = process.cwd();
|
|
const read = (path) => readFileSync(join(root, path), 'utf8');
|
|
|
|
for (const suffix of ['up', 'down', 'verify']) {
|
|
assert.ok(
|
|
existsSync(join(root,
|
|
`database/migrations/2026081109_m09d3_product_storage.${suffix}.sql`)),
|
|
`M09-D3 ${suffix} migration is missing`
|
|
);
|
|
}
|
|
|
|
const migration = read('database/migrations/2026081109_m09d3_product_storage.up.sql');
|
|
for (const pattern of [
|
|
'qipai_product_storage_records',
|
|
'qipai_product_storage_items',
|
|
'qipai_product_storage_events',
|
|
'qipai_product_storage_event_items',
|
|
'PARTIALLY_RETRIEVED',
|
|
'claim_credential_hash',
|
|
'remaining_quantity',
|
|
'uq_qipai_product_storage_source_order',
|
|
'PRODUCT_STORAGE_EVENT_IMMUTABLE',
|
|
'PRODUCT_STORAGE_EVENT_ITEM_IMMUTABLE',
|
|
'goods.storage.read',
|
|
'goods.storage.manage'
|
|
]) assert.ok(migration.includes(pattern), `M09-D3 migration is missing ${pattern}`);
|
|
|
|
const service = read('backend/src/products/product-storage-service.ts');
|
|
for (const pattern of [
|
|
'ProductStorageService',
|
|
'createFromOrder',
|
|
'createManual',
|
|
'rotateCredential',
|
|
'expireDueForManagement',
|
|
'timingSafeEqual',
|
|
'FOR UPDATE',
|
|
'FOR UPDATE SKIP LOCKED',
|
|
'qipai_product_storage_event_items',
|
|
'qipai_outbox_events',
|
|
'qipai_audit_logs',
|
|
'PRODUCT_STORAGE_SOURCE_ORDER_ALREADY_STORED'
|
|
]) assert.ok(service.includes(pattern), `product storage service is missing ${pattern}`);
|
|
assert.match(service, /createHash\('sha256'\)/);
|
|
assert.match(service, /beginTransaction\(\)[\s\S]*commit\(\)[\s\S]*rollback\(\)/);
|
|
assert.match(service, /claimCredential: null/);
|
|
|
|
const routes = read('backend/src/routes/product-storages.ts');
|
|
for (const pattern of [
|
|
'/app-api/product-storages',
|
|
'/admin-api',
|
|
'/app-api/management',
|
|
'/credential/rotate',
|
|
'/expire-due',
|
|
'goods.storage.read',
|
|
'goods.storage.manage'
|
|
]) assert.ok(routes.includes(pattern), `product storage routes are missing ${pattern}`);
|
|
|
|
const app = read('backend/src/app.ts');
|
|
const server = read('backend/src/server.ts');
|
|
assert.match(app, /registerProductStorageRoutes/);
|
|
assert.match(app, /productStorages\?: ProductStorageRouteOptions/);
|
|
assert.match(server, /new ProductStorageService\(pool\)/);
|
|
|
|
const rbac = read('backend/src/auth/rbac-repository.ts');
|
|
assert.match(rbac, /'goods\.storage\.read', 'goods\.storage\.manage'/);
|
|
const packageJson = read('backend/package.json');
|
|
assert.match(packageJson, /product-storage-service\.test\.mjs/);
|
|
assert.match(packageJson, /product-storage-route\.test\.mjs/);
|
|
|
|
const mysqlTest = read('backend/tests/mysql-migration-roundtrip.test.mjs');
|
|
for (const pattern of [
|
|
'assertProductStorageLifecycle',
|
|
'only one concurrent full retrieval may succeed',
|
|
'PRODUCT_STORAGE_CREDENTIAL_INVALID',
|
|
'PRODUCT_STORAGE_EVENT_IMMUTABLE',
|
|
'PRODUCT_STORAGE_EVENT_ITEM_IMMUTABLE',
|
|
'expireDueForManagement'
|
|
]) assert.ok(mysqlTest.includes(pattern), `live MySQL test is missing ${pattern}`);
|
|
|
|
console.log(
|
|
'PASS: M09-D3 storage ownership, one-time credential, partial/concurrent retrieval and immutable-ledger gates are present.'
|
|
);
|