feat(M09-D3): 完成商品寄存与安全取出闭环
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
ProductStorageError,
|
||||
ProductStorageService,
|
||||
productStorageCredentialDigest,
|
||||
resolveProductStorageRetrieval
|
||||
} from '../dist/products/product-storage-service.js';
|
||||
|
||||
const credential = 'claim_credential_abcdefghijklmnopqrstuvwxyz012345';
|
||||
const digest = productStorageCredentialDigest(credential);
|
||||
assert.match(digest, /^[0-9a-f]{64}$/);
|
||||
assert.equal(digest.includes(credential), false);
|
||||
assert.equal(productStorageCredentialDigest(credential), digest);
|
||||
assert.notEqual(productStorageCredentialDigest(`${credential}x`), digest);
|
||||
|
||||
assert.deepEqual(resolveProductStorageRetrieval('STORED', 5, 2), {
|
||||
remainingQuantity: 3, status: 'PARTIALLY_RETRIEVED'
|
||||
});
|
||||
assert.deepEqual(resolveProductStorageRetrieval('PARTIALLY_RETRIEVED', 3, 3), {
|
||||
remainingQuantity: 0, status: 'RETRIEVED'
|
||||
});
|
||||
for (const [status, quantity] of [['RETRIEVED', 1], ['EXPIRED', 1], ['CANCELLED', 1]]) {
|
||||
assert.throws(
|
||||
() => resolveProductStorageRetrieval(status, 1, quantity),
|
||||
(error) => error instanceof ProductStorageError
|
||||
&& error.code === 'PRODUCT_STORAGE_NOT_ACTIVE'
|
||||
);
|
||||
}
|
||||
assert.throws(
|
||||
() => resolveProductStorageRetrieval('STORED', 2, 3),
|
||||
(error) => error instanceof ProductStorageError
|
||||
&& error.code === 'PRODUCT_STORAGE_QUANTITY_EXCEEDED'
|
||||
);
|
||||
|
||||
const manager = {
|
||||
tenantId: '7', userId: '31',
|
||||
access: {
|
||||
roles: ['STORE_ADMIN'], capabilities: ['goods.storage.read', 'goods.storage.manage'],
|
||||
storeIds: ['11']
|
||||
},
|
||||
source: 'MANAGEMENT', traceId: 'm09d3-storage-service',
|
||||
ip: '127.0.0.1', userAgent: 'storage-service-test'
|
||||
};
|
||||
const noSqlPool = {
|
||||
async execute() { throw new Error('SQL must not run for rejected input'); },
|
||||
async getConnection() { throw new Error('transaction must not start for rejected input'); }
|
||||
};
|
||||
const service = new ProductStorageService(noSqlPool, {
|
||||
now: () => new Date('2026-08-11T07:00:00.000Z'),
|
||||
credentialFactory: () => credential
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => service.createManual(manager, {
|
||||
requestId: 'storage-create-duplicate', storeId: '11', memberId: '21',
|
||||
expiresAt: new Date('2026-08-12T07:00:00.000Z'),
|
||||
items: [{ skuId: '5', quantity: 1 }, { skuId: '5', quantity: 1 }]
|
||||
}),
|
||||
(error) => error instanceof ProductStorageError
|
||||
&& error.code === 'PRODUCT_STORAGE_ITEMS_INVALID'
|
||||
);
|
||||
await assert.rejects(
|
||||
() => service.createFromOrder({ ...manager, source: 'CUSTOMER', userId: '21' }, {
|
||||
requestId: 'storage-expiry-invalid', sourceOrderId: '101',
|
||||
expiresAt: new Date('2026-08-11T07:00:30.000Z')
|
||||
}),
|
||||
(error) => error instanceof ProductStorageError
|
||||
&& error.code === 'PRODUCT_STORAGE_EXPIRY_INVALID'
|
||||
);
|
||||
await assert.rejects(
|
||||
() => service.retrieve({ ...manager, source: 'CUSTOMER', userId: '21' }, '301', {
|
||||
requestId: 'storage-retrieve-invalid-credential', claimCredential: 'short',
|
||||
items: [{ storageItemId: '401', quantity: 1 }]
|
||||
}),
|
||||
(error) => error instanceof ProductStorageError
|
||||
&& error.code === 'PRODUCT_STORAGE_CREDENTIAL_INVALID'
|
||||
);
|
||||
|
||||
console.log('PASS: M09-D3 credential hashing, retrieval state transitions and input boundaries work.');
|
||||
Reference in New Issue
Block a user