openvibers/openvibe.extensions Commits

Companion: never show or try a code its source keeps out of extensions (#12)

OP OpenVibers committed 7 days ago · f1daf407ad · 3 files, +30 −5

Defence in depth for the Coupons source terms: OpenVibe.Coupons already
withholds DealNews codes from extension answers, but the Companion now drops
any code its answer marks `extension_excluded` before it is shown, and again
before "Try these codes" types one into a shop's coupon box.

Changes

ddiff --git a/browser/lib/controller.js b/browser/lib/controller.js
iindex b2aee36..7fc5d92 100644
---- a/browser/lib/controller.js
++++ b/browser/lib/controller.js
@@@ -44,6 +44,15 @@
return 'Something went wrong. Try again later.';
}
++ /**
++ * The codes the Companion may show: a code whose source's terms keep it out of browser extensions never
++ * reaches the person, even when an answer carries one. OpenVibe.Coupons already leaves those out
++ * (server/domain/source-policy.js); the Companion drops any marked `extension_excluded` all the same.
++ */
++ function visible(coupons) {
++ return (coupons || []).filter(function (c) { return c && !c.extension_excluded; });
++ }
++
function createController(deps) {
const api = deps.api;
const platform = deps.platform;
@@@ -74,7 +83,7 @@
const merchant = await api.resolve(hostname);
if (!merchant) return { state: 'no_merchant', host: hostname };
const out = await api.coupons(merchant.id);
-- return { state: 'merchant', host: hostname, merchant: out.merchant, coupons: out.coupons || [], connected: connected };
++ return { state: 'merchant', host: hostname, merchant: out.merchant, coupons: visible(out.coupons), connected: connected };
} catch (err) {
return { state: 'error', host: hostname, message: messageFor(err), code: (err && err.code) || null };
}
@@@ -125,7 +134,7 @@
*/
async tryCodes(coupons) {
const rank = { reported_working: 0, unknown: 1 };
-- const list = (coupons || []).filter(function (c) { return c && c.code && c.status !== 'reported_not_working'; })
++ const list = visible(coupons).filter(function (c) { return c.code && c.status !== 'reported_not_working'; })
.slice().sort(function (a, b) { return (rank[a.status] == null ? 2 : rank[a.status]) - (rank[b.status] == null ? 2 : rank[b.status]); });
if (!list.length) return { ok: false, message: 'There are no codes to try here.', results: [] };
let r;
ddiff --git a/test/autoapply.test.js b/test/autoapply.test.js
iindex fe540a6..19756de 100644
---- a/test/autoapply.test.js
++++ b/test/autoapply.test.js
@@@ -16,7 +16,7 @@ const { createApiClient } = require('../browser/lib/api');
const { createPlatform } = require('../browser/lib/platform');
const { createController } = require('../browser/lib/controller');
const config = require('../browser/lib/config');
--const { check, done, startStub, fakeExt } = require('./helpers/harness');
++const { check, done, startStub, fakeExt, COUPON } = require('./helpers/harness');
const field = (attrs, labels = []) => ({
tagName: 'INPUT', type: attrs.type || 'text', name: attrs.name || '', id: attrs.id || '',
@@@ -80,6 +80,22 @@ const field = (attrs, labels = []) => ({
assert.deepStrictEqual(r.results.map((x) => [x.code, x.outcome, x.saved]), [['GOOD', 'failed', null], ['SAVE10', 'worked', 10]]);
});
++ await check('a code its source keeps out of browser extensions is never shown or tried, even if an answer carries one', async () => {
++ const kept = { ...COUPON, extension_excluded: false };
++ const withheld = { ...COUPON, id: 'cpn_01K5ZK3M9P2Q4R6S8T0V2W4X6W', code: 'DEALNEWS10', extension_excluded: true };
++ const st = await startStub({ coupons: [kept, withheld] });
++ const ext = fakeExt({ url: 'https://shop.acme-outdoor.com/cart' });
++ const platform = createPlatform(ext, { ...config, API_ORIGIN: st.origin });
++ const api = createApiClient({ origin: st.origin, fetch, getToken: platform.storage.getToken });
++ const c = createController({ api, platform, host, handoff, isRental: handoffLib.isRental });
++ const vm = await c.open();
++ assert.deepStrictEqual(vm.coupons.map((x) => x.code), ['TRAIL15'], 'the marked code is dropped before showing');
++ const r = await c.tryCodes([withheld]);
++ assert.strictEqual(r.ok, false, 'and never typed into the page');
++ assert.strictEqual(ext.log.filter((l) => l[0] === 'scripting.executeScript').length, 0);
++ await st.close();
++ });
++
await check('no coupon box, or no Apply button: it says so and presses nothing', async () => {
for (const [reason, re] of [['no_field', /No coupon box on this page/], ['no_apply', /no Apply button next to it, so nothing was pressed/]]) {
const p = popup({ url: 'https://shop.example.com/checkout', scriptResult: { ok: false, reason, before: 20, after: 20, results: [] } });
ddiff --git a/test/helpers/harness.js b/test/helpers/harness.js
iindex e09c158..a86d50d 100644
---- a/test/helpers/harness.js
++++ b/test/helpers/harness.js
@@@ -24,7 +24,7 @@ const NO_REPORT_TOKEN = `cpx_${'n'.repeat(43)}`;
const SUBMITTER_TOKEN = `cpx_${'s'.repeat(43)}`; // the person who submitted COUPON
/** A stub of the Coupons API with the behaviour the extension relies on. */
--function startStub({ delayMs = 0 } = {}) {
++function startStub({ delayMs = 0, coupons = [COUPON] } = {}) {
const requests = [];
const server = http.createServer((req, res) => {
const chunks = [];
@@@ -47,7 +47,7 @@ function startStub({ delayMs = 0 } = {}) {
if (host === 'busy-shop.com') return send(429, { code: 'lookup.rate_limited' });
return send(404, { code: 'merchant.not_found' });
}
-- if (req.method === 'GET' && u.pathname === `/api/v1/merchants/${MERCHANT.id}/coupons`) return send(200, { merchant: MERCHANT, coupons: [COUPON] });
++ if (req.method === 'GET' && u.pathname === `/api/v1/merchants/${MERCHANT.id}/coupons`) return send(200, { merchant: MERCHANT, coupons, left_out: 0 });
if (req.method === 'POST' && u.pathname === `/api/v1/coupons/${COUPON.id}/report`) {
if (!auth) return send(401, { code: 'auth.required' });
if (auth === `Bearer ${NO_REPORT_TOKEN}`) return send(403, { code: 'token.scope' });