Files
r-zakarya a7ad81c708 refactor(js): extract favicon module + add faviconHost tests (§3.1)
Sixth slice of the app.js split. Moves the favicon fetch/cache section
(faviconHost, saveEntryIcon, ensureEntryFavicon, backfillFavicons,
clearAllFavicons) to js/app.favicon.js. Pure declarations, no top-level
side effects → loads before app.js.

- Code moved byte-for-byte; no duplicate const; syntax OK on all 7 app parts.
- NEW: js/tests/favicon.test.js — 7 tests for faviconHost, the pure
  URL→validated-hostname function that decides which domain is sent to the
  DuckDuckGo proxy (a bug there leaks the wrong host). Covers scheme/www/
  path/port stripping, non-hostname rejection, malformed dotting, unsafe
  chars, and the 253-char DNS cap.
- Fixed an inaccurate source comment surfaced by the tests: it claimed raw
  IPs "stay valid", but the TLD rule /\.[a-z]{2,}$/ rejects a numeric final
  label, so IPs get no favicon lookup (fine). Test pins the real behaviour.
- Suite: 55 → 62 tests, all green. Assets regenerated (8 ordered JS files).

app.js: 11936 → 9790 lines (6 modules extracted).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 16:58:16 +01:00

63 lines
2.8 KiB
JavaScript

// faviconHost tests (js/app.favicon.js) — the pure URL→validated-hostname
// function that decides which domain (if any) is sent to the DuckDuckGo
// favicon proxy. A bug here means a DNS-ish leak of the wrong host, so the
// validation rules are worth pinning down.
const test = require('node:test');
const assert = require('node:assert/strict');
const { loadApp } = require('./harness.js');
const T = loadApp().__test;
const host = (s) => T.faviconHost(s);
test('faviconHost: strips scheme, www, path, port', () => {
assert.equal(host('https://www.github.com/user/repo'), 'github.com');
assert.equal(host('http://example.com'), 'example.com');
assert.equal(host('www.example.com'), 'example.com');
assert.equal(host('example.com:8443/login'), 'example.com');
assert.equal(host('HTTPS://WWW.Example.COM'), 'example.com');
});
test('faviconHost: keeps subdomains (SLD fallback is the caller\'s job)', () => {
assert.equal(host('chat.qwen.ai'), 'chat.qwen.ai');
assert.equal(host('git.example.co.uk'), 'git.example.co.uk');
});
test('faviconHost: rejects non-hostnames → "" (no lookup, no leak)', () => {
assert.equal(host('Gitea'), ''); // brand name, no dot
assert.equal(host('my work pwd'), ''); // spaces
assert.equal(host('localhost'), ''); // no TLD
assert.equal(host(''), '');
assert.equal(host(null), '');
assert.equal(host(undefined), '');
});
test('faviconHost: rejects malformed dotting', () => {
assert.equal(host('.example.com'), ''); // leading dot
assert.equal(host('example.com.'), ''); // trailing dot
assert.equal(host('a..b.com'), ''); // double dot
assert.equal(host('example.x'), ''); // 1-char TLD
});
test('faviconHost: rejects hostname-unsafe characters', () => {
assert.equal(host('exa mple.com'), '');
assert.equal(host('exam_ple.com'), ''); // underscore not hostname-safe
assert.equal(host('ex+ample.com'), '');
});
test('faviconHost: raw IPs are rejected (numeric final label fails the TLD rule)', () => {
// NOTE: the source comment claims IPs "stay valid", but the TLD check
// /\.[a-z]{2,}$/ requires a LETTERS final label, so a numeric last octet
// is rejected → '' (no lookup). Actual behaviour, pinned here; the code
// comment is inaccurate. Harmless: DDG has no favicon for a bare IP anyway.
assert.equal(host('1.2.3.44'), '');
assert.equal(host('192.168.0.1'), '');
});
test('faviconHost: enforces the 253-char DNS cap', () => {
const long = 'a'.repeat(250) + '.com'; // 254 chars
assert.equal(host(long), '');
const ok = 'a'.repeat(60) + '.com'; // well under cap, valid
assert.equal(host(ok), ok);
});