openvibers/openvibe.coupons Commits
POST /merchants waits for its code sync to commit: store().tx(syncMerchant) was started and abandoned (no await, no catch), so the API answered 201 while the sync was in flight and a failure was swallowed, leaving the merchant active with its waiting codes unpublished. It is awaited, as the staff path already was; a failed sync answers 500 through run(). test/lifecycle.test.js: a rejecting syncMerchant answers 500 with no unhandled rejection (fails on the old code with 201). Found by the estate-wide floating-promise sweep; reviewed independently (VERDICT: SHIP). npm test 13/13 test files passed.
Changes
@@@ -194,7 +194,7 @@ function createApi(ctx) {const body = req.body || {};const status = body.status === 'pending' ? 'pending' : 'active';const m = await merchants.create(body, { status, actor });-- store().tx(async () => await coupons.syncMerchant(m));++ await store().tx(async () => await coupons.syncMerchant(m));return { merchant: { ...await merchantView(m), status: m.status } };}, 201));@@@ -205,6 +205,23 @@ const DAY = 24 * HOUR;assert.strictEqual((await t.get('/api/v1/merchants/resolve?host=shop.new-gadgets.net')).status, 200);});++ await check('POST /merchants waits for syncMerchant: a failed sync reaches the caller as a 500, not a swallowed rejection', async () => {++ const real = t.ctx.coupons.syncMerchant;++ const swallowed = [];++ const onUnhandled = (err) => { swallowed.push(err); };++ process.on('unhandledRejection', onUnhandled);++ t.ctx.coupons.syncMerchant = async () => { throw new Error('sync failed'); };++ try {++ const res = await t.get('/api/v1/merchants', { as: t.staffToken(), json: { name: 'Failed Sync Shop', domains: [{ host: 'failed-sync.com', include_subdomains: true, path_prefix: '' }] } });++ assert.strictEqual(res.status, 500, `the sync failure must reach the caller: ${res.status} ${res.text}`);++ assert.deepStrictEqual(swallowed.map((e) => e.message), [], 'the sync rejection must not be left unhandled');++ } finally {++ t.ctx.coupons.syncMerchant = real;++ await new Promise((r) => setTimeout(r, 20));++ process.removeListener('unhandledRejection', onUnhandled);++ }++ });++await check('duplicates add evidence to the same code; a taken-down code cannot be resubmitted', async () => {const a = await t.submit(bob, { host: 'acme-outdoor.com', code: 'trail15', title: 'Same code, other case', evidence_url: 'https://forum.example-deals.org/t/1' });assert.strictEqual(a.status, 200);
@@ -194,7 +194,7 @@ function createApi(ctx) {
@@ -194,7 +194,7 @@ function createApi(ctx) {
const body = req.body || {};
const body = req.body || {};
const status = body.status === 'pending' ? 'pending' : 'active';
const status = body.status === 'pending' ? 'pending' : 'active';
const m = await merchants.create(body, { status, actor });
const m = await merchants.create(body, { status, actor });
- store().tx(async () => await coupons.syncMerchant(m));
+ await store().tx(async () => await coupons.syncMerchant(m));
return { merchant: { ...await merchantView(m), status: m.status } };
return { merchant: { ...await merchantView(m), status: m.status } };
}, 201));
}, 201));
@@ -205,6 +205,23 @@ const DAY = 24 * HOUR;
@@ -205,6 +205,23 @@ const DAY = 24 * HOUR;
assert.strictEqual((await t.get('/api/v1/merchants/resolve?host=shop.new-gadgets.net')).status, 200);
assert.strictEqual((await t.get('/api/v1/merchants/resolve?host=shop.new-gadgets.net')).status, 200);
});
});
+ await check('POST /merchants waits for syncMerchant: a failed sync reaches the caller as a 500, not a swallowed rejection', async () => {
+ const real = t.ctx.coupons.syncMerchant;
+ const swallowed = [];
+ const onUnhandled = (err) => { swallowed.push(err); };
+ process.on('unhandledRejection', onUnhandled);
+ t.ctx.coupons.syncMerchant = async () => { throw new Error('sync failed'); };
+ try {
+ const res = await t.get('/api/v1/merchants', { as: t.staffToken(), json: { name: 'Failed Sync Shop', domains: [{ host: 'failed-sync.com', include_subdomains: true, path_prefix: '' }] } });
+ assert.strictEqual(res.status, 500, `the sync failure must reach the caller: ${res.status} ${res.text}`);
+ assert.deepStrictEqual(swallowed.map((e) => e.message), [], 'the sync rejection must not be left unhandled');
+ } finally {
+ t.ctx.coupons.syncMerchant = real;
+ await new Promise((r) => setTimeout(r, 20));
+ process.removeListener('unhandledRejection', onUnhandled);
+ }
+ });
+
await check('duplicates add evidence to the same code; a taken-down code cannot be resubmitted', async () => {
await check('duplicates add evidence to the same code; a taken-down code cannot be resubmitted', async () => {
const a = await t.submit(bob, { host: 'acme-outdoor.com', code: 'trail15', title: 'Same code, other case', evidence_url: 'https://forum.example-deals.org/t/1' });
const a = await t.submit(bob, { host: 'acme-outdoor.com', code: 'trail15', title: 'Same code, other case', evidence_url: 'https://forum.example-deals.org/t/1' });
assert.strictEqual(a.status, 200);
assert.strictEqual(a.status, 200);