mirror of https://github.com/garrytan/gstack.git
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');
|
|
});
|
|
});
|