// Copyright (c) 2026 Gordon Bolton. MIT License. /** * Tests for server/utils/sanitizeError.js * * Critical security tests: verify that API keys, tokens, passwords and other * secrets are NEVER leaked in error messages returned to clients or written * to logs. Every pattern here represents a real credential type used in the * sofarr stack (SABnzbd apikey, Emby tokens, qBittorrent basic-auth URLs). */ import sanitizeError from '../../server/utils/sanitizeError.js'; describe('sanitizeError', () => { describe('query-param secrets', () => { it('redacts ?apikey= values', () => { const err = new Error('Request failed: https://sabnzbd.local/api?apikey=abc123secret&output=json'); expect(sanitizeError(err)).toContain('[REDACTED]'); expect(sanitizeError(err)).not.toContain('abc123secret'); }); it('redacts &apikey= mid-URL', () => { const err = new Error('GET https://host/path?mode=queue&apikey=SUPERSECRET&output=json'); expect(sanitizeError(err)).not.toContain('SUPERSECRET'); expect(sanitizeError(err)).toContain('[REDACTED]'); }); it('redacts ?token= values', () => { const err = new Error('https://api.example.com/data?token=tok_private99'); expect(sanitizeError(err)).not.toContain('tok_private99'); }); it('redacts ?password= values', () => { const err = new Error('Auth failed: https://service.local?password=hunter2'); expect(sanitizeError(err)).not.toContain('hunter2'); }); it('redacts ?api_key= values', () => { const err = new Error('https://sonarr.local/api/v3/series?api_key=e583d270f89846478e42'); expect(sanitizeError(err)).not.toContain('e583d270f89846478e42'); }); it('preserves non-secret query params', () => { const result = sanitizeError(new Error('GET /api?mode=queue&output=json')); expect(result).toContain('mode=queue'); expect(result).toContain('output=json'); }); }); describe('HTTP auth headers', () => { it('redacts X-Api-Key header values', () => { const err = new Error('Request: x-api-key: e583d270f89846478e42dd3cf90bfb00'); expect(sanitizeError(err)).not.toContain('e583d270f89846478e42dd3cf90bfb00'); expect(sanitizeError(err)).toContain('[REDACTED]'); }); it('redacts X-MediaBrowser-Token header values', () => { const err = new Error('x-mediabrowser-token: b862f3a43f4c417285043a11aa28b1f7'); expect(sanitizeError(err)).not.toContain('b862f3a43f4c417285043a11aa28b1f7'); }); it('redacts Authorization header values', () => { const err = new Error('authorization: MediaBrowser Token="abc123", DeviceId="xyz"'); expect(sanitizeError(err)).not.toContain('abc123'); }); }); describe('bearer tokens', () => { it('redacts Bearer token values', () => { const err = new Error('Error: bearer eyJhbGciOiJIUzI1NiJ9.payload.sig'); expect(sanitizeError(err)).not.toContain('eyJhbGciOiJIUzI1NiJ9'); expect(sanitizeError(err)).toContain('bearer [REDACTED]'); }); it('is case-insensitive for BEARER', () => { const err = new Error('BEARER TOKEN_VALUE_HERE'); expect(sanitizeError(err)).not.toContain('TOKEN_VALUE_HERE'); }); }); describe('basic-auth URLs', () => { it('redacts user:pass@ in URLs', () => { const err = new Error('GET http://admin:b053288369XX!@qbittorrent.local/api'); expect(sanitizeError(err)).not.toContain('b053288369XX!'); expect(sanitizeError(err)).not.toContain('admin:'); expect(sanitizeError(err)).toContain('//[REDACTED]@'); }); it('handles https:// basic auth', () => { const err = new Error('https://user:s3cr3t@service.local/path'); expect(sanitizeError(err)).not.toContain('s3cr3t'); }); }); describe('edge cases', () => { it('handles non-Error input (plain string)', () => { const result = sanitizeError('plain string error'); expect(typeof result).toBe('string'); }); it('handles null gracefully', () => { expect(() => sanitizeError(null)).not.toThrow(); }); it('handles undefined gracefully', () => { expect(() => sanitizeError(undefined)).not.toThrow(); }); it('preserves non-sensitive error messages unchanged', () => { const err = new Error('Connection refused: ECONNREFUSED 127.0.0.1:8080'); const result = sanitizeError(err); expect(result).toContain('ECONNREFUSED'); expect(result).toContain('127.0.0.1:8080'); }); it('does not leak stack traces (returns message only)', () => { const err = new Error('something went wrong'); const result = sanitizeError(err); expect(result).not.toContain('at '); expect(result).not.toContain('.js:'); }); }); });