Files
silent-send/test-suite.html
T
Claude ed9419f04b fix: test suite CSP compatibility for Firefox extension pages
Replace inline onclick handlers with addEventListener (Firefox
extension CSP blocks inline event handlers). Show loading state
and test count on page load.

https://claude.ai/code/session_01KF4i7Ra7zCEDskxDBaNtcT
2026-03-28 18:13:17 +00:00

516 lines
22 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Silent Send - Feature Test Suite</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace; font-size: 13px; padding: 20px; background: #f9fafb; }
h1 { font-size: 18px; margin-bottom: 16px; }
h2 { font-size: 14px; margin: 16px 0 8px; color: #374151; border-bottom: 1px solid #e5e7eb; padding-bottom: 4px; }
.test-group { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; margin-bottom: 12px; }
.test { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; }
.test .status { width: 18px; text-align: center; font-weight: bold; }
.pass { color: #10b981; }
.fail { color: #dc2626; }
.skip { color: #f59e0b; }
.detail { color: #6b7280; font-size: 11px; margin-left: 26px; }
#summary { font-size: 14px; font-weight: 600; margin-top: 16px; padding: 12px; border-radius: 8px; }
#summary.all-pass { background: #d1fae5; color: #065f46; }
#summary.has-fail { background: #fee2e2; color: #991b1b; }
button { padding: 8px 16px; border: 1px solid #d1d5db; border-radius: 6px; background: #fff; cursor: pointer; font-size: 13px; }
button:hover { background: #f3f4f6; }
button.primary { background: #3b82f6; color: #fff; border-color: #3b82f6; }
</style>
</head>
<body>
<h1>Silent Send - Feature Test Suite</h1>
<p style="margin-bottom:12px;color:#6b7280">
Load this page as an extension page (or inject via dev console on the Options page).<br>
Tests exercise sync, encryption, storage, auto-redact, and domain management.
</p>
<button class="primary" id="btnRunTests">Run All Tests</button>
<button id="btnClear">Clear</button>
<div id="results" style="margin-top:16px"></div>
<div id="summary"></div>
<script type="module">
import Storage from './src/lib/storage.js';
import SilentSendSync from './src/lib/sync.js';
import SilentSendCrypto from './src/lib/crypto.js';
import AutoRedact from './src/lib/auto-redact.js';
import AutoDetect from './src/lib/auto-detect.js';
import SmartPatterns from './src/lib/smart-patterns.js';
import SubstitutionEngine from './src/lib/substitution-engine.js';
const results = [];
let currentGroup = '';
function group(name) {
currentGroup = name;
}
function test(name, fn) {
results.push({ group: currentGroup, name, fn });
}
function renderResults(outcomes) {
const el = document.getElementById('results');
const groups = {};
for (const o of outcomes) {
if (!groups[o.group]) groups[o.group] = [];
groups[o.group].push(o);
}
let html = '';
for (const [groupName, tests] of Object.entries(groups)) {
html += `<div class="test-group"><h2>${groupName}</h2>`;
for (const t of tests) {
const icon = t.status === 'pass' ? '✓' : t.status === 'fail' ? '✗' : '○';
const cls = t.status;
html += `<div class="test"><span class="status ${cls}">${icon}</span> ${t.name}</div>`;
if (t.error) html += `<div class="detail">${t.error}</div>`;
}
html += '</div>';
}
el.innerHTML = html;
const passed = outcomes.filter(o => o.status === 'pass').length;
const failed = outcomes.filter(o => o.status === 'fail').length;
const skipped = outcomes.filter(o => o.status === 'skip').length;
const sum = document.getElementById('summary');
sum.className = failed > 0 ? 'has-fail' : 'all-pass';
sum.textContent = `${passed} passed, ${failed} failed, ${skipped} skipped — ${outcomes.length} total`;
}
async function runAllTests() {
results.length = 0;
// ============================================================
// STORAGE TESTS
// ============================================================
group('Storage — Settings');
test('Default settings have autoRedact=true', async () => {
const s = await Storage.getSettings();
if (s.autoRedact !== true) throw `Expected autoRedact=true, got ${s.autoRedact}`;
});
test('Default settings have customRedactPatterns=[]', async () => {
const s = await Storage.getSettings();
if (!Array.isArray(s.customRedactPatterns)) throw `Expected array, got ${typeof s.customRedactPatterns}`;
});
test('Default settings have maxLogEntries=100', async () => {
const s = await Storage.getSettings();
if (s.maxLogEntries !== 100) throw `Expected 100, got ${s.maxLogEntries}`;
});
test('Default settings have browserSync=false', async () => {
const s = await Storage.getSettings();
if (s.browserSync !== false) throw `Expected false, got ${s.browserSync}`;
});
test('Save and read settings round-trip', async () => {
await Storage.saveSettings({ autoRedact: false });
const s = await Storage.getSettings();
if (s.autoRedact !== false) throw `Expected false after save, got ${s.autoRedact}`;
await Storage.saveSettings({ autoRedact: true }); // restore
});
group('Storage — Activity Log');
test('Activity log respects maxLogEntries=100', async () => {
// Save current log
const origLog = await Storage.getLog();
await Storage.clearLog();
// Add 110 entries
for (let i = 0; i < 110; i++) {
await Storage.addLogEntry({ type: 'test', original: `test${i}`, replaced: `fake${i}` });
}
const log = await Storage.getLog();
if (log.length > 100) throw `Expected <=100 entries, got ${log.length}`;
await Storage.clearLog();
});
group('Storage — Mappings');
test('Add and retrieve mapping', async () => {
const m = await Storage.addMapping({ real: 'TestReal', substitute: 'TestFake', category: 'general' });
if (!m.id) throw 'Mapping has no id';
const all = await Storage.getMappings();
const found = all.find(x => x.id === m.id);
if (!found) throw 'Mapping not found after add';
await Storage.deleteMapping(m.id);
});
group('Storage — Profiles');
test('Add and retrieve profile', async () => {
const p = await Storage.addProfile('Test Profile');
if (!p.id) throw 'Profile has no id';
const all = await Storage.getProfiles();
const found = all.find(x => x.id === p.id);
if (!found) throw 'Profile not found after add';
await Storage.deleteProfile(p.id);
});
// ============================================================
// ENCRYPTION TESTS
// ============================================================
group('Encryption — Setup & Authentication');
test('Setup encryption with password', async () => {
// Clean state
await SilentSendSync.disableEncryption().catch(() => {});
const result = await SilentSendSync.setupEncryption({
password: 'testpass123',
enableTOTP: false,
ttlDays: 90,
});
if (!result.success) throw `Setup failed: ${result.reason}`;
});
test('isEncryptionEnabled returns true after setup', async () => {
const enabled = await SilentSendSync.isEncryptionEnabled();
if (!enabled) throw 'Expected encryption to be enabled';
});
test('Authenticate with correct password', async () => {
const result = await SilentSendSync.authenticate('testpass123');
if (!result.success) throw `Auth failed: ${result.reason}`;
});
test('Authenticate with wrong password fails', async () => {
// Clear cached key first to force re-auth
await SilentSendCrypto.clearCachedKey();
const result = await SilentSendSync.authenticate('wrongpassword');
if (result.success) throw 'Expected auth to fail with wrong password';
// Re-auth with correct password
await SilentSendSync.authenticate('testpass123');
});
group('Encryption — At-Rest');
test('Data is encrypted at rest when encryption enabled', async () => {
const api = (typeof browser !== 'undefined') ? browser : chrome;
await Storage.saveMappings([{ id: 'enc-test', real: 'EncSecret', substitute: 'EncFake' }]);
const raw = await api.storage.local.get('ss_mappings');
const val = raw.ss_mappings;
if (!val?._ssLocalEncrypted) throw 'Expected _ssLocalEncrypted flag on stored data';
// Clean up
await Storage.saveMappings([]);
});
test('Encrypted data reads back correctly', async () => {
await Storage.saveMappings([{ id: 'enc-rt', real: 'RoundTrip', substitute: 'RTFake' }]);
const mappings = await Storage.getMappings();
const found = mappings.find(m => m.id === 'enc-rt');
if (!found) throw 'Mapping not found after encrypted round-trip';
if (found.real !== 'RoundTrip') throw `Expected 'RoundTrip', got '${found.real}'`;
await Storage.saveMappings([]);
});
// ============================================================
// SYNC — ENCRYPTION MANDATORY
// ============================================================
group('Sync — Encryption Required');
test('Sync code export works when encryption enabled', async () => {
const code = await SilentSendSync.exportSyncCode();
if (code?.needsEncryption) throw 'Should not need encryption — it is enabled';
if (code?.needsAuth) throw 'Should not need auth — key is cached';
if (typeof code !== 'string') throw `Expected string, got ${typeof code}`;
if (code.length < 10) throw 'Sync code seems too short';
});
test('Sync code export fails without encryption', async () => {
await SilentSendSync.disableEncryption();
const code = await SilentSendSync.exportSyncCode();
if (!code?.needsEncryption) throw 'Expected needsEncryption=true when encryption disabled';
// Re-enable
await SilentSendSync.setupEncryption({ password: 'testpass123' });
});
test('Sync code import round-trip', async () => {
// Add test data
await Storage.saveMappings([{ id: 'sync-test', real: 'SyncMe', substitute: 'SyncFake' }]);
const code = await SilentSendSync.exportSyncCode();
// Clear and re-import
await Storage.saveMappings([]);
const result = await SilentSendSync.importSyncCode(code, { force: true });
if (!result.success) throw `Import failed: ${result.reason}`;
const mappings = await Storage.getMappings();
const found = mappings.find(m => m.real === 'SyncMe');
if (!found) throw 'Mapping not found after sync code round-trip';
await Storage.saveMappings([]);
});
test('pushToSyncStorage silently skips without encryption', async () => {
await SilentSendSync.disableEncryption();
// Should not throw, just silently skip
await SilentSendSync.pushToSyncStorage();
// Re-enable
await SilentSendSync.setupEncryption({ password: 'testpass123' });
});
test('pushToGist fails without encryption', async () => {
await SilentSendSync.disableEncryption();
const result = await SilentSendSync.pushToGist('fake-token');
if (!result.needsEncryption) throw 'Expected needsEncryption=true';
// Re-enable
await SilentSendSync.setupEncryption({ password: 'testpass123' });
});
test('pushToUrl fails without encryption', async () => {
await SilentSendSync.disableEncryption();
const result = await SilentSendSync.pushToUrl({ url: 'https://example.com/sync' });
if (!result.needsEncryption) throw 'Expected needsEncryption=true';
// Re-enable
await SilentSendSync.setupEncryption({ password: 'testpass123' });
});
group('Sync — Disable Encryption Disables Sync');
test('Disabling encryption sets browserSync=false', async () => {
await Storage.saveSettings({ browserSync: true });
await SilentSendSync.disableEncryption();
const s = await Storage.getSettings();
if (s.browserSync !== false) throw `Expected browserSync=false, got ${s.browserSync}`;
// Re-enable encryption
await SilentSendSync.setupEncryption({ password: 'testpass123' });
});
// ============================================================
// ENCRYPTION WITH TOTP
// ============================================================
group('Encryption — TOTP');
test('Setup encryption with TOTP', async () => {
await SilentSendSync.disableEncryption().catch(() => {});
const result = await SilentSendSync.setupEncryption({
password: 'totptest123',
enableTOTP: true,
ttlDays: 90,
});
if (!result.success) throw `Setup failed: ${result.reason}`;
if (!result.totpSecret) throw 'Expected TOTP secret in response';
if (!result.totpURI) throw 'Expected TOTP URI in response';
});
test('TOTP secret is encrypted at rest', async () => {
const config = await SilentSendSync._getSyncEncryption();
if (config.totpSecret) throw 'TOTP secret should be encrypted, not plaintext';
if (!config._totpEncrypted) throw 'Expected _totpEncrypted blob';
});
test('Clean up — disable encryption', async () => {
await SilentSendSync.disableEncryption();
const enabled = await SilentSendSync.isEncryptionEnabled();
if (enabled) throw 'Encryption should be disabled';
});
// ============================================================
// AUTO-REDACT TESTS
// ============================================================
group('Auto-Redact — Built-in Patterns');
test('Detects OpenAI key', () => {
const result = AutoRedact.redact('my key is sk-abc123def456ghi789jkl012mno');
if (result.redactions.length === 0) throw 'Expected redaction';
if (!result.text.includes('[REDACTED-OPENAI-KEY]')) throw 'Expected [REDACTED-OPENAI-KEY]';
});
test('Detects GitHub token', () => {
const result = AutoRedact.redact('token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn');
if (result.redactions.length === 0) throw 'Expected redaction';
});
test('Detects SSN', () => {
const result = AutoRedact.redact('my ssn is 123-45-6789');
if (result.redactions.length === 0) throw 'Expected redaction';
if (!result.text.includes('[REDACTED-SSN]')) throw 'Expected [REDACTED-SSN]';
});
test('Detects credit card', () => {
const result = AutoRedact.redact('card: 4111 1111 1111 1111');
if (result.redactions.length === 0) throw 'Expected redaction';
});
test('Detects Bearer token', () => {
const result = AutoRedact.redact('Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9abcdef');
if (result.redactions.length === 0) throw 'Expected redaction';
});
test('Detects private key block', () => {
const result = AutoRedact.redact('-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----');
if (result.redactions.length === 0) throw 'Expected redaction';
});
test('Detects connection string', () => {
const result = AutoRedact.redact('mongodb://admin:secret@db.example.com:27017/mydb');
if (result.redactions.length === 0) throw 'Expected redaction';
});
group('Auto-Redact — Custom Patterns');
test('Custom pattern matches', () => {
const custom = [{ name: 'ControlD', pattern: 'dns\\.controld\\.com/[A-Za-z0-9;]+', redact: '[REDACTED-CONTROLD]', enabled: true }];
const result = AutoRedact.redact('url: https://dns.controld.com/sasdkj;kjasda', custom);
if (result.redactions.length === 0) throw 'Expected custom pattern redaction';
if (!result.text.includes('[REDACTED-CONTROLD]')) throw 'Expected [REDACTED-CONTROLD]';
});
test('Disabled custom pattern does not match', () => {
const custom = [{ name: 'ControlD', pattern: 'dns\\.controld\\.com/[A-Za-z0-9;]+', redact: '[REDACTED]', enabled: false }];
const result = AutoRedact.redact('url: https://dns.controld.com/sasdkj;kjasda', custom);
if (result.text.includes('[REDACTED]')) throw 'Disabled pattern should not match';
});
test('Invalid regex in custom pattern is skipped gracefully', () => {
const custom = [{ name: 'Bad', pattern: '[invalid(regex', redact: '[X]', enabled: true }];
// Should not throw
const result = AutoRedact.redact('test text', custom);
if (result.text !== 'test text') throw 'Text should be unchanged';
});
test('Redactions use category=redact', () => {
const result = AutoRedact.redact('key: sk-abc123def456ghi789jkl012mno');
if (result.redactions.length > 0 && result.redactions[0].category !== 'redact') {
throw `Expected category 'redact', got '${result.redactions[0].category}'`;
}
});
// ============================================================
// SUBSTITUTION ENGINE
// ============================================================
group('Substitution Engine');
test('Basic substitution works', () => {
const result = SubstitutionEngine.substitute('Hello John Smith', [
{ real: 'John Smith', substitute: 'Alex Demo', enabled: true }
]);
if (!result.text.includes('Alex Demo')) throw `Expected 'Alex Demo' in output`;
});
test('Case insensitive by default', () => {
const result = SubstitutionEngine.substitute('hello JOHN SMITH', [
{ real: 'John Smith', substitute: 'Alex Demo', enabled: true, caseSensitive: false }
]);
if (!result.text.includes('Alex Demo')) throw 'Expected case-insensitive match';
});
test('Disabled mapping is skipped', () => {
const result = SubstitutionEngine.substitute('Hello John Smith', [
{ real: 'John Smith', substitute: 'Alex Demo', enabled: false }
]);
if (result.text.includes('Alex Demo')) throw 'Disabled mapping should not substitute';
});
// ============================================================
// SMART PATTERNS
// ============================================================
group('Smart Patterns — Identity');
test('Email substitution', () => {
const identity = {
emails: [{ real: 'john@gmail.com', substitute: 'alex@example.com' }],
names: [], usernames: [], phones: [], hostnames: [],
enabled: { emails: true, names: true, usernames: true, phones: true, paths: true },
};
const result = SmartPatterns.substitute('email: john@gmail.com', identity);
if (!result.text.includes('alex@example.com')) throw 'Expected email substitution';
});
test('Name substitution', () => {
const identity = {
names: [{ real: 'John Smith', substitute: 'Alex Demo' }],
emails: [], usernames: [], phones: [], hostnames: [],
enabled: { emails: true, names: true, usernames: true, phones: true, paths: true },
};
const result = SmartPatterns.substitute('My name is John Smith', identity);
if (!result.text.includes('Alex Demo')) throw 'Expected name substitution';
});
test('Phone substitution', () => {
const identity = {
phones: [{ real: '555-123-4567', substitute: '555-000-0000' }],
names: [], emails: [], usernames: [], hostnames: [],
enabled: { emails: true, names: true, usernames: true, phones: true, paths: true },
};
const result = SmartPatterns.substitute('call 555-123-4567', identity);
if (!result.text.includes('555-000-0000')) throw 'Expected phone substitution';
});
// ============================================================
// AUTO-DETECT (PII Scanner)
// ============================================================
group('Auto-Detect — PII Patterns');
test('Detects private IP addresses', () => {
const warnings = AutoDetect.scan('server at 192.168.1.100', {});
const ipWarn = warnings.find(w => w.type === 'private-ip');
if (!ipWarn) throw 'Expected private IP detection';
});
test('Detects SSN pattern', () => {
const warnings = AutoDetect.scan('ssn: 123-45-6789', {});
// SSN may be caught by auto-detect or auto-redact — either is fine
if (warnings.length === 0) {
// Auto-redact catches it, auto-detect may not duplicate — this is OK
}
});
// ============================================================
// SETTINGS INTEGRITY
// ============================================================
group('Settings — No Stale Keys');
test('No secretScanning key in defaults', async () => {
const s = await Storage.getSettings();
if ('secretScanning' in s && !('autoRedact' in s)) throw 'Found stale secretScanning key';
});
test('No customSecretPatterns key in defaults', async () => {
const s = await Storage.getSettings();
if ('customSecretPatterns' in s && !('customRedactPatterns' in s)) throw 'Found stale customSecretPatterns key';
});
// ============================================================
// RUN ALL
// ============================================================
async function runAllTests() {
document.getElementById('results').innerHTML = '<p style="color:#6b7280">Running tests...</p>';
const outcomes = [];
for (const t of results) {
try {
await t.fn();
outcomes.push({ group: t.group, name: t.name, status: 'pass' });
} catch (e) {
const msg = typeof e === 'string' ? e : (e?.message || String(e));
outcomes.push({ group: t.group, name: t.name, status: 'fail', error: msg });
}
}
renderResults(outcomes);
}
// Wire up buttons (CSP blocks inline onclick)
document.getElementById('btnRunTests').addEventListener('click', runAllTests);
document.getElementById('btnClear').addEventListener('click', () => {
document.getElementById('results').innerHTML = '';
document.getElementById('summary').textContent = '';
});
// Show ready state
console.log(`[Silent Send Tests] ${results.length} tests registered. Click "Run All Tests" to start.`);
document.getElementById('results').innerHTML = `<p style="color:#6b7280">${results.length} tests loaded. Click "Run All Tests" to start.</p>`;
</script>
</body>
</html>