Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/components/PrivacySafetyModal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest';
import { THIRD_PARTY_DISCLOSURES } from './PrivacySafetyModal';
import { WALK_TARGETS } from '../utils/walkTest';
import { FAMILY_ENDPOINTS } from '../utils/dualStack';

/**
* The disclosure list is a contract with the user, and the rule is that a new
* probe endpoint gets disclosed in the same commit that adds it.
*
* A rule enforced by discipline alone is a rule that eventually slips, and it
* did: swapping Facebook for Atlassian in `WALK_TARGETS` updated the README
* table and left the in-app modal naming a host the browser no longer contacts
* while omitting one it now does. Nothing failed, because prose does not
* typecheck. These tests are the thing that fails instead.
*
* They deliberately check both directions. A missing host understates what
* leaves the browser, which is the serious one. A stale host overstates it,
* which is the kind of error that quietly erodes the list's credibility until
* nobody reads it.
*/

const allDisclosedText = THIRD_PARTY_DISCLOSURES.map((d) => `${d.host} ${d.receives}`).join('\n');
const allDisclosedHosts = THIRD_PARTY_DISCLOSURES.map((d) => d.host).join(', ');

describe('THIRD_PARTY_DISCLOSURES', () => {
it('names every host Walk & Test probes', () => {
for (const target of WALK_TARGETS) {
expect(allDisclosedHosts, `${target.host} is probed but not disclosed`).toContain(
target.host,
);
}
});

it('does not name a Walk & Test host that was removed from the target list', () => {
// Meta domains were dropped because blocklists made them a false alarm. If
// the disclosure still claims the browser contacts them, the list is
// describing an app that no longer exists.
const dropped = ['www.facebook.com', 'www.instagram.com'];
for (const host of dropped) {
expect(allDisclosedHosts, `${host} is disclosed but no longer probed`).not.toContain(host);
}
});

it('names every dual-stack probe host', () => {
for (const endpoint of FAMILY_ENDPOINTS) {
expect(allDisclosedHosts, `${endpoint.host} is probed but not disclosed`).toContain(
endpoint.host,
);
}
});

it('gives every row a host and a description of what that host receives', () => {
for (const row of THIRD_PARTY_DISCLOSURES) {
expect(row.host.trim().length).toBeGreaterThan(0);
// A row that names a host without saying what it gets is decoration.
expect(row.receives.trim().length).toBeGreaterThan(20);
}
});

it('says what the walk sends, not just who it sends it to', () => {
// The specifics a reader needs to judge the trade: how often, how much, and
// whether they are identifiable while it happens.
expect(allDisclosedText).toContain('Walk & Test');
expect(allDisclosedText).toMatch(/no cookies|credentials/i);
});
});
15 changes: 8 additions & 7 deletions src/components/PrivacySafetyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,16 @@ export const THIRD_PARTY_DISCLOSURES: { host: string; receives: string }[] = [
},
{
host:
'www.google.com, www.youtube.com, www.netflix.com, www.facebook.com, www.amazon.com, ' +
'outlook.office365.com, teams.microsoft.com, zoom.us, login.salesforce.com, slack.com',
'www.google.com, www.youtube.com, www.netflix.com, www.amazon.com, ' +
'outlook.office365.com, teams.microsoft.com, zoom.us, login.salesforce.com, slack.com, ' +
'www.atlassian.com',
receives:
'Your IP, repeatedly, for as long as a Walk & Test run lasts — every one of them is probed ' +
'once per round, so a ten-minute walk at the default interval is roughly two hundred ' +
'requests to each. Each request is a HEAD for one small public file and carries no cookies ' +
'(`credentials: \'omit\'`), so these hosts see an address and a TLS handshake rather than a ' +
'logged-in user. Several of them are advertising businesses; the IP and the timing pattern ' +
'are still theirs to log.',
'once per round, so a ten-minute walk is a few hundred requests to each, up to roughly six ' +
'hundred at the one-second interval. Each request is a HEAD for one small public file and ' +
'carries no cookies (credentials: omit), so these hosts see an address and a TLS handshake ' +
'rather than a logged-in user. Several of them are advertising businesses; the IP and the ' +
'timing pattern are still theirs to log.',
},
{
host: 'api.github.com, httpbin.org, cloudflare.com, dns.google',
Expand Down
Loading