← back to Homesonspec

apps/mobile/lib/tracker-block.test.mjs

138 lines

/**
 * Behavioural tests for TRACKER_BLOCK — the script actually injected into every WebView.
 *
 * `tracker-policy.test.mjs` only covers isTrackerUrl(), which is the NATIVE-side URL
 * filter. That left the thing doing most of the work — the injected page script that
 * neuters gtag/fbq and intercepts fetch/XHR/Image/sendBeacon — with no test at all.
 * Since the App Store "Data Not Collected" claim rests on this script actually working,
 * "it looks right" was not good enough.
 *
 * These tests run the real script text in a sandbox with a fake window/document and
 * assert observable behaviour: tracker requests are dropped, first-party requests are
 * untouched.
 */
import assert from 'node:assert/strict';
import test from 'node:test';
import vm from 'node:vm';

import { buildTrackerBlock } from './tracker-block.ts';
import { TRACKER_HOSTS } from './tracker-policy.ts';

// Test the script exactly as shipped: built from the REAL host list.
const TRACKER_BLOCK = buildTrackerBlock(TRACKER_HOSTS);

const TRACKER = 'https://www.googletagmanager.com/gtag/js?id=G-ZGFNZ3RQ6S';
const PIXEL = 'https://connect.facebook.net/en_US/fbevents.js';
const FIRST_PARTY = 'https://homesonspec.com/api/listings';

/** Build a minimal browser-ish sandbox, run TRACKER_BLOCK in it, return the context. */
function runBlock() {
  const calls = { fetch: [], beacon: [], xhrSend: [], imgSrc: [] };

  class FakeXHR {
    open(method, url) { this.__url = url; }
    send() { calls.xhrSend.push(this.__url); }
  }

  class FakeImage {
    constructor() { this._src = ''; }
    set src(v) { this._src = v; calls.imgSrc.push(v); }
    get src() { return this._src; }
  }

  const el = () => ({ _attrs: {}, setAttribute(k, v) { this._attrs[k] = v; }, getAttribute(k) { return this._attrs[k]; } });

  const sandbox = {
    calls,
    Response: class { constructor(body, init) { this.body = body; this.status = init?.status ?? 200; } },
    Promise,
    URL,
    XMLHttpRequest: FakeXHR,
    MutationObserver: class { observe() {} },
    console,
    document: {
      documentElement: {},
      createElement: (tag) => ({ tagName: String(tag).toUpperCase(), ...el() }),
    },
  };
  sandbox.window = sandbox;
  sandbox.Image = FakeImage;
  sandbox.HTMLImageElement = FakeImage;
  sandbox.HTMLElement = function () {};
  sandbox.location = { href: 'https://homesonspec.com/' };
  sandbox.navigator = { sendBeacon: (url) => { calls.beacon.push(url); return true; } };
  sandbox.fetch = (input) => { calls.fetch.push(input?.url ?? input); return Promise.resolve('REAL'); };

  vm.createContext(sandbox);
  vm.runInContext(TRACKER_BLOCK, sandbox);
  return sandbox;
}

test('gtag / fbq / dataLayer are neutered into no-ops', () => {
  const w = runBlock();
  assert.equal(typeof w.gtag, 'function');
  assert.equal(typeof w.fbq, 'function');
  assert.doesNotThrow(() => w.gtag('event', 'page_view', { send_to: 'G-XXXX' }));
  assert.doesNotThrow(() => w.fbq('track', 'PageView'));
  assert.doesNotThrow(() => w.dataLayer.push({ event: 'x' }));
  // dataLayer.push must not actually accumulate — a real GTM would drain it.
  assert.equal(w.dataLayer.length, 0, 'dataLayer.push should be a no-op, not a real push');
});

test('fetch to a tracker host is dropped; first-party fetch passes through', async () => {
  const w = runBlock();
  const blocked = await w.fetch(TRACKER);
  assert.equal(blocked.status, 204, 'tracker fetch should be short-circuited with an empty 204');
  assert.deepEqual(w.calls.fetch, [], 'the real fetch must never see a tracker URL');

  const real = await w.fetch(FIRST_PARTY);
  assert.equal(real, 'REAL', 'first-party fetch must reach the real implementation');
  assert.deepEqual(w.calls.fetch, [FIRST_PARTY]);
});

test('sendBeacon (GA4 default transport) is dropped for trackers only', () => {
  const w = runBlock();
  assert.equal(w.navigator.sendBeacon('https://region1.google-analytics.com/g/collect'), false);
  assert.deepEqual(w.calls.beacon, [], 'no tracker beacon may reach the real sendBeacon');

  assert.equal(w.navigator.sendBeacon(FIRST_PARTY), true);
  assert.deepEqual(w.calls.beacon, [FIRST_PARTY]);
});

test('XHR to a tracker never sends; first-party XHR does', () => {
  const w = runBlock();
  const bad = new w.XMLHttpRequest();
  bad.open('POST', PIXEL);
  bad.send();
  assert.deepEqual(w.calls.xhrSend, [], 'tracker XHR must not send');

  const good = new w.XMLHttpRequest();
  good.open('GET', FIRST_PARTY);
  good.send();
  assert.deepEqual(w.calls.xhrSend, [FIRST_PARTY]);
});

test('Image() pixel to a tracker is dropped; a first-party image still loads', () => {
  const w = runBlock();
  const px = new w.Image();
  px.src = 'https://www.facebook.com/tr?id=123&ev=PageView';
  assert.deepEqual(w.calls.imgSrc, [], 'the Facebook tr pixel must never get a real src');

  const img = new w.Image();
  img.src = 'https://homesonspec.com/hero.jpg';
  assert.deepEqual(w.calls.imgSrc, ['https://homesonspec.com/hero.jpg']);
});

test('a subdomain of a tracker host is blocked too (not just the bare host)', async () => {
  const w = runBlock();
  const r = await w.fetch('https://analytics.google-analytics.com/g/collect?v=2');
  assert.equal(r.status, 204);
  assert.deepEqual(w.calls.fetch, []);
});

test('a lookalike domain is NOT blocked (the filter must not over-reach)', async () => {
  const w = runBlock();
  const r = await w.fetch('https://notgoogletagmanager.com/thing.js');
  assert.equal(r, 'REAL', 'a lookalike host must pass through — over-blocking breaks real sites');
});