Files
gstack/browse/test/sidebar-unit.test.ts
T
Garry Tan ac80abdc34 test: sidebar agent test suite (layers 1-2)
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>
2026-03-26 18:46:16 -06:00

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');
});
});