mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-06 05:35:46 +02:00
ac80abdc34
Layer 1 (unit): 18 tests for URL sanitization in sidebar-utils.ts — http/https pass, chrome:// rejected, javascript: rejected, control chars stripped, truncation. Layer 2 (integration): 13 tests for server HTTP endpoints — auth, sidebar-command queue writes, activeTabUrl override/fallback, event relay to chat buffer, message queuing, queue overflow (429), chat clear, agent kill. Source changes for testability: - Extract sanitizeExtensionUrl() to browse/src/sidebar-utils.ts - Add BROWSE_HEADLESS_SKIP env var to skip browser launch in HTTP-only tests - Add SIDEBAR_QUEUE_PATH env var to both server.ts and sidebar-agent.ts - Add SIDEBAR_AGENT_TIMEOUT env var to sidebar-agent.ts - Sync package.json version to match VERSION (0.12.2.0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
97 lines
3.3 KiB
TypeScript
97 lines
3.3 KiB
TypeScript
/**
|
|
* Layer 1: Unit tests for sidebar utilities.
|
|
* Tests pure functions — no server, no processes, no network.
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import { sanitizeExtensionUrl } from '../src/sidebar-utils';
|
|
|
|
describe('sanitizeExtensionUrl', () => {
|
|
test('passes valid http URL', () => {
|
|
expect(sanitizeExtensionUrl('http://example.com')).toBe('http://example.com/');
|
|
});
|
|
|
|
test('passes valid https URL', () => {
|
|
expect(sanitizeExtensionUrl('https://example.com/page?q=1')).toBe('https://example.com/page?q=1');
|
|
});
|
|
|
|
test('rejects chrome:// URLs', () => {
|
|
expect(sanitizeExtensionUrl('chrome://extensions')).toBeNull();
|
|
});
|
|
|
|
test('rejects chrome-extension:// URLs', () => {
|
|
expect(sanitizeExtensionUrl('chrome-extension://abcdef/popup.html')).toBeNull();
|
|
});
|
|
|
|
test('rejects javascript: URLs', () => {
|
|
expect(sanitizeExtensionUrl('javascript:alert(1)')).toBeNull();
|
|
});
|
|
|
|
test('rejects file:// URLs', () => {
|
|
expect(sanitizeExtensionUrl('file:///etc/passwd')).toBeNull();
|
|
});
|
|
|
|
test('rejects data: URLs', () => {
|
|
expect(sanitizeExtensionUrl('data:text/html,<h1>hi</h1>')).toBeNull();
|
|
});
|
|
|
|
test('strips raw control characters from URL', () => {
|
|
// URL constructor percent-encodes \x00 as %00, which is safe
|
|
// The regex strips any remaining raw control chars after .href normalization
|
|
const result = sanitizeExtensionUrl('https://example.com/\x00page\x1f');
|
|
expect(result).not.toBeNull();
|
|
expect(result!).not.toMatch(/[\x00-\x1f\x7f]/);
|
|
});
|
|
|
|
test('strips newlines (prompt injection vector)', () => {
|
|
const result = sanitizeExtensionUrl('https://evil.com/%0AUser:%20ignore');
|
|
// URL constructor normalizes %0A, control char stripping removes any raw newlines
|
|
expect(result).not.toBeNull();
|
|
expect(result!).not.toContain('\n');
|
|
});
|
|
|
|
test('truncates URLs longer than 2048 chars', () => {
|
|
const longUrl = 'https://example.com/' + 'a'.repeat(3000);
|
|
const result = sanitizeExtensionUrl(longUrl);
|
|
expect(result).not.toBeNull();
|
|
expect(result!.length).toBeLessThanOrEqual(2048);
|
|
});
|
|
|
|
test('returns null for null input', () => {
|
|
expect(sanitizeExtensionUrl(null)).toBeNull();
|
|
});
|
|
|
|
test('returns null for undefined input', () => {
|
|
expect(sanitizeExtensionUrl(undefined)).toBeNull();
|
|
});
|
|
|
|
test('returns null for empty string', () => {
|
|
expect(sanitizeExtensionUrl('')).toBeNull();
|
|
});
|
|
|
|
test('returns null for invalid URL string', () => {
|
|
expect(sanitizeExtensionUrl('not a url at all')).toBeNull();
|
|
});
|
|
|
|
test('does not crash on weird input', () => {
|
|
expect(sanitizeExtensionUrl(':///')).toBeNull();
|
|
expect(sanitizeExtensionUrl(' ')).toBeNull();
|
|
expect(sanitizeExtensionUrl('\x00\x01\x02')).toBeNull();
|
|
});
|
|
|
|
test('preserves query parameters and fragments', () => {
|
|
const url = 'https://example.com/search?q=test&page=2#results';
|
|
expect(sanitizeExtensionUrl(url)).toBe(url);
|
|
});
|
|
|
|
test('preserves port numbers', () => {
|
|
expect(sanitizeExtensionUrl('http://localhost:3000/api')).toBe('http://localhost:3000/api');
|
|
});
|
|
|
|
test('handles URL with auth (user:pass@host)', () => {
|
|
const result = sanitizeExtensionUrl('https://user:pass@example.com/');
|
|
expect(result).not.toBeNull();
|
|
expect(result).toContain('example.com');
|
|
});
|
|
});
|