openvibers/openvibe.extensions Commits
Companion: never show or try a code its source keeps out of extensions (#12)
Changes
@@@ -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;@@@ -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: [] } });@@@ -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' });
@@ -44,6 +44,15 @@
@@ -44,6 +44,15 @@
return 'Something went wrong. Try again later.';
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) {
function createController(deps) {
const api = deps.api;
const api = deps.api;
const platform = deps.platform;
const platform = deps.platform;
@@ -74,7 +83,7 @@
@@ -74,7 +83,7 @@
const merchant = await api.resolve(hostname);
const merchant = await api.resolve(hostname);
if (!merchant) return { state: 'no_merchant', host: hostname };
if (!merchant) return { state: 'no_merchant', host: hostname };
const out = await api.coupons(merchant.id);
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) {
} catch (err) {
return { state: 'error', host: hostname, message: messageFor(err), code: (err && err.code) || null };
return { state: 'error', host: hostname, message: messageFor(err), code: (err && err.code) || null };
}
}
@@ -125,7 +134,7 @@
@@ -125,7 +134,7 @@
*/
*/
async tryCodes(coupons) {
async tryCodes(coupons) {
const rank = { reported_working: 0, unknown: 1 };
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]); });
.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: [] };
if (!list.length) return { ok: false, message: 'There are no codes to try here.', results: [] };
let r;
let r;
@@ -16,7 +16,7 @@ const { createApiClient } = require('../browser/lib/api');
@@ -16,7 +16,7 @@ const { createApiClient } = require('../browser/lib/api');
const { createPlatform } = require('../browser/lib/platform');
const { createPlatform } = require('../browser/lib/platform');
const { createController } = require('../browser/lib/controller');
const { createController } = require('../browser/lib/controller');
const config = require('../browser/lib/config');
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 = []) => ({
const field = (attrs, labels = []) => ({
tagName: 'INPUT', type: attrs.type || 'text', name: attrs.name || '', id: attrs.id || '',
tagName: 'INPUT', type: attrs.type || 'text', name: attrs.name || '', id: attrs.id || '',
@@ -80,6 +80,22 @@ const field = (attrs, labels = []) => ({
@@ -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]]);
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 () => {
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/]]) {
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: [] } });
const p = popup({ url: 'https://shop.example.com/checkout', scriptResult: { ok: false, reason, before: 20, after: 20, results: [] } });
@@ -24,7 +24,7 @@ const NO_REPORT_TOKEN = `cpx_${'n'.repeat(43)}`;
@@ -24,7 +24,7 @@ const NO_REPORT_TOKEN = `cpx_${'n'.repeat(43)}`;
const SUBMITTER_TOKEN = `cpx_${'s'.repeat(43)}`; // the person who submitted COUPON
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. */
/** 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 requests = [];
const server = http.createServer((req, res) => {
const server = http.createServer((req, res) => {
const chunks = [];
const chunks = [];
@@ -47,7 +47,7 @@ function startStub({ delayMs = 0 } = {}) {
@@ -47,7 +47,7 @@ function startStub({ delayMs = 0 } = {}) {
if (host === 'busy-shop.com') return send(429, { code: 'lookup.rate_limited' });
if (host === 'busy-shop.com') return send(429, { code: 'lookup.rate_limited' });
return send(404, { code: 'merchant.not_found' });
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 (req.method === 'POST' && u.pathname === `/api/v1/coupons/${COUPON.id}/report`) {
if (!auth) return send(401, { code: 'auth.required' });
if (!auth) return send(401, { code: 'auth.required' });
if (auth === `Bearer ${NO_REPORT_TOKEN}`) return send(403, { code: 'token.scope' });
if (auth === `Bearer ${NO_REPORT_TOKEN}`) return send(403, { code: 'token.scope' });