UNPKG

guardz-event

Version:

Type-safe event handling with runtime validation using guardz for unsafe data from 3rd parties

476 lines 22.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /// <reference types="jest" /> const safe_event_never_throws_1 = require("../utils/safe-event-never-throws"); // Mock type guards for testing const isUserMessage = (data) => { return (typeof data === 'object' && data !== null && typeof data.id === 'number' && typeof data.name === 'string' && typeof data.email === 'string' && typeof data.isActive === 'boolean'); }; const isStockUpdate = (data) => { return (typeof data === 'object' && data !== null && typeof data.symbol === 'string' && typeof data.price === 'number' && typeof data.change === 'number'); }; describe('Error Handling and Edge Cases', () => { describe('Null and Undefined Events', () => { describe('Legacy API', () => { it('should handle null events gracefully', () => { const handler = (0, safe_event_never_throws_1.safePostMessage)({ guard: isUserMessage, allowedOrigins: ['https://trusted-domain.com'], }); const result = handler(null); expect(result.status).toBe(safe_event_never_throws_1.Status.ERROR); if (result.status === safe_event_never_throws_1.Status.ERROR) { expect(result.code).toBe(500); expect(result.message).toContain('Invalid event'); } }); it('should handle undefined events gracefully', () => { const handler = (0, safe_event_never_throws_1.safePostMessage)({ guard: isUserMessage, allowedOrigins: ['https://trusted-domain.com'], }); const result = handler(undefined); expect(result.status).toBe(safe_event_never_throws_1.Status.ERROR); if (result.status === safe_event_never_throws_1.Status.ERROR) { expect(result.code).toBe(500); expect(result.message).toContain('Invalid event'); } }); }); describe('Ergonomic API', () => { it('should handle null events gracefully', () => { const onSuccess = jest.fn(); const onTypeMismatch = jest.fn(); const onError = jest.fn(); const handler = (0, safe_event_never_throws_1.safePostMessageListener)(isUserMessage, { allowedOrigins: ['https://trusted-domain.com'], onSuccess, onTypeMismatch, onError, }); handler(null); expect(onSuccess).not.toHaveBeenCalled(); expect(onTypeMismatch).not.toHaveBeenCalled(); expect(onError).toHaveBeenCalledWith(expect.stringContaining('Invalid event'), expect.objectContaining({ identifier: 'postMessage', })); }); it('should handle undefined events gracefully', () => { const onSuccess = jest.fn(); const onTypeMismatch = jest.fn(); const onError = jest.fn(); const handler = (0, safe_event_never_throws_1.safePostMessageListener)(isUserMessage, { allowedOrigins: ['https://trusted-domain.com'], onSuccess, onTypeMismatch, onError, }); handler(undefined); expect(onSuccess).not.toHaveBeenCalled(); expect(onTypeMismatch).not.toHaveBeenCalled(); expect(onError).toHaveBeenCalledWith(expect.stringContaining('Invalid event'), expect.objectContaining({ identifier: 'postMessage', })); }); }); }); describe('Invalid Event Data Types', () => { describe('Legacy API', () => { it('should handle non-object data', () => { const handler = (0, safe_event_never_throws_1.safePostMessage)({ guard: isUserMessage, allowedOrigins: ['https://trusted-domain.com'], }); const stringEvent = new MessageEvent('message', { origin: 'https://trusted-domain.com', data: 'not-an-object', }); const result = handler(stringEvent); expect(result.status).toBe(safe_event_never_throws_1.Status.ERROR); if (result.status === safe_event_never_throws_1.Status.ERROR) { expect(result.code).toBe(500); expect(result.message).toContain('Validation failed'); } }); it('should handle primitive data types', () => { const handler = (0, safe_event_never_throws_1.safePostMessage)({ guard: isUserMessage, allowedOrigins: ['https://trusted-domain.com'], }); const numberEvent = new MessageEvent('message', { origin: 'https://trusted-domain.com', data: 123, }); const result = handler(numberEvent); expect(result.status).toBe(safe_event_never_throws_1.Status.ERROR); if (result.status === safe_event_never_throws_1.Status.ERROR) { expect(result.code).toBe(500); expect(result.message).toContain('Validation failed'); } }); it('should handle boolean data', () => { const handler = (0, safe_event_never_throws_1.safePostMessage)({ guard: isUserMessage, allowedOrigins: ['https://trusted-domain.com'], }); const booleanEvent = new MessageEvent('message', { origin: 'https://trusted-domain.com', data: true, }); const result = handler(booleanEvent); expect(result.status).toBe(safe_event_never_throws_1.Status.ERROR); if (result.status === safe_event_never_throws_1.Status.ERROR) { expect(result.code).toBe(500); expect(result.message).toContain('Validation failed'); } }); }); describe('Ergonomic API', () => { it('should call onTypeMismatch for non-object data', () => { const onSuccess = jest.fn(); const onTypeMismatch = jest.fn(); const onError = jest.fn(); const handler = (0, safe_event_never_throws_1.safePostMessageListener)(isUserMessage, { allowedOrigins: ['https://trusted-domain.com'], onSuccess, onTypeMismatch, onError, }); const stringEvent = new MessageEvent('message', { origin: 'https://trusted-domain.com', data: 'not-an-object', }); handler(stringEvent); expect(onSuccess).not.toHaveBeenCalled(); expect(onTypeMismatch).toHaveBeenCalledWith(expect.stringContaining('Validation failed')); expect(onError).not.toHaveBeenCalled(); }); }); }); describe('Malformed Event Objects', () => { describe('Legacy API', () => { it('should handle events without data property', () => { const handler = (0, safe_event_never_throws_1.safePostMessage)({ guard: isUserMessage, allowedOrigins: ['https://trusted-domain.com'], }); const malformedEvent = { type: 'message', origin: 'https://trusted-domain.com', // Missing data property }; const result = handler(malformedEvent); expect(result.status).toBe(safe_event_never_throws_1.Status.ERROR); if (result.status === safe_event_never_throws_1.Status.ERROR) { expect(result.code).toBe(500); expect(result.message).toContain('Validation failed'); } }); it('should handle events without origin property', () => { const handler = (0, safe_event_never_throws_1.safePostMessage)({ guard: isUserMessage, allowedOrigins: ['https://trusted-domain.com'], }); const malformedEvent = new MessageEvent('message', { data: { id: 1, name: 'John', email: 'john@example.com', isActive: true, }, // Missing origin property - will be undefined }); const result = handler(malformedEvent); expect(result.status).toBe(safe_event_never_throws_1.Status.ERROR); if (result.status === safe_event_never_throws_1.Status.ERROR) { expect(result.code).toBe(403); expect(result.message).toContain('not allowed'); } }); }); }); describe('Type Guard Errors', () => { describe('Legacy API', () => { it('should handle type guard that throws an error', () => { const throwingGuard = (data) => { if (typeof data === 'object' && data !== null) { throw new Error('Type guard error'); } return false; }; const onError = jest.fn(); const handler = (0, safe_event_never_throws_1.safePostMessage)({ guard: throwingGuard, tolerance: true, onError, }); const event = new MessageEvent('message', { origin: 'https://trusted-domain.com', data: { id: 1 }, }); const result = handler(event); expect(result.status).toBe(safe_event_never_throws_1.Status.ERROR); expect(onError).toHaveBeenCalledWith(expect.stringContaining('Unexpected error during event validation'), expect.objectContaining({ identifier: 'postMessage', originalError: expect.any(Error), })); }); }); describe('Ergonomic API', () => { it('should handle type guard that throws an error', () => { const throwingGuard = (data) => { if (typeof data === 'object' && data !== null) { throw new Error('Type guard error'); } return false; }; const onSuccess = jest.fn(); const onTypeMismatch = jest.fn(); const onError = jest.fn(); const handler = (0, safe_event_never_throws_1.safePostMessageListener)(throwingGuard, { tolerance: true, allowedOrigins: ['https://trusted-domain.com'], onSuccess, onTypeMismatch, onError, }); const event = new MessageEvent('message', { origin: 'https://trusted-domain.com', data: { id: 1 }, }); handler(event); expect(onSuccess).not.toHaveBeenCalled(); // When type guard throws, it may call both onError (for the exception) and onTypeMismatch (for the 500 result) // This is acceptable behavior as the error is handled in multiple places expect(onError).toHaveBeenCalledWith(expect.stringContaining('Unexpected error during event validation'), expect.objectContaining({ identifier: 'postMessage', })); }); }); }); describe('Callback Error Handling', () => { describe('Ergonomic API', () => { it('should handle onSuccess callback that throws', () => { const throwingOnSuccess = jest.fn().mockImplementation(() => { throw new Error('Success callback error'); }); const onTypeMismatch = jest.fn(); const onError = jest.fn(); const handler = (0, safe_event_never_throws_1.safePostMessageListener)(isUserMessage, { allowedOrigins: ['https://trusted-domain.com'], onSuccess: throwingOnSuccess, onTypeMismatch, onError, }); const event = new MessageEvent('message', { origin: 'https://trusted-domain.com', data: { id: 1, name: 'John', email: 'john@example.com', isActive: true, }, }); // Should not throw, but should handle the error gracefully expect(() => handler(event)).not.toThrow(); expect(throwingOnSuccess).toHaveBeenCalled(); }); it('should handle onTypeMismatch callback that throws', () => { const onSuccess = jest.fn(); const throwingOnTypeMismatch = jest.fn().mockImplementation(() => { throw new Error('Type mismatch callback error'); }); const onError = jest.fn(); const handler = (0, safe_event_never_throws_1.safePostMessageListener)(isUserMessage, { allowedOrigins: ['https://trusted-domain.com'], onSuccess, onTypeMismatch: throwingOnTypeMismatch, onError, }); const event = new MessageEvent('message', { origin: 'https://trusted-domain.com', data: 'invalid-data', }); // Should not throw, but should handle the error gracefully expect(() => handler(event)).not.toThrow(); expect(throwingOnTypeMismatch).toHaveBeenCalled(); }); it('should handle onSecurityViolation callback that throws', () => { const onSuccess = jest.fn(); const onTypeMismatch = jest.fn(); const throwingOnSecurityViolation = jest.fn().mockImplementation(() => { throw new Error('Security violation callback error'); }); const onError = jest.fn(); const handler = (0, safe_event_never_throws_1.safePostMessageListener)(isUserMessage, { allowedOrigins: ['https://trusted-domain.com'], onSuccess, onTypeMismatch, onSecurityViolation: throwingOnSecurityViolation, onError, }); const event = new MessageEvent('message', { origin: 'https://malicious-site.com', data: { id: 1, name: 'John', email: 'john@example.com', isActive: true, }, }); // Should not throw, but should handle the error gracefully expect(() => handler(event)).not.toThrow(); expect(throwingOnSecurityViolation).toHaveBeenCalled(); }); }); }); describe('Edge Cases for Different Event Types', () => { describe('WebSocket Events', () => { it('should handle WebSocket events with null data', () => { const handler = (0, safe_event_never_throws_1.safeWebSocket)({ guard: isStockUpdate, }); const nullDataEvent = new MessageEvent('message', { data: null, }); const result = handler(nullDataEvent); expect(result.status).toBe(safe_event_never_throws_1.Status.ERROR); if (result.status === safe_event_never_throws_1.Status.ERROR) { expect(result.code).toBe(500); expect(result.message).toContain('Validation failed'); } }); }); describe('DOM Events', () => { it('should handle DOM events with missing properties', () => { const handler = (0, safe_event_never_throws_1.safeDOMEvent)({ guard: isUserMessage, }); const minimalEvent = new Event('click'); const result = handler(minimalEvent); expect(result.status).toBe(safe_event_never_throws_1.Status.ERROR); if (result.status === safe_event_never_throws_1.Status.ERROR) { expect(result.code).toBe(500); expect(result.message).toContain('Validation failed'); } }); }); describe('Custom Events', () => { it('should handle CustomEvents with null detail', () => { const handler = (0, safe_event_never_throws_1.safeCustomEvent)({ guard: isStockUpdate, }); const nullDetailEvent = new CustomEvent('stock-update', { detail: null, }); const result = handler(nullDetailEvent); expect(result.status).toBe(safe_event_never_throws_1.Status.ERROR); if (result.status === safe_event_never_throws_1.Status.ERROR) { expect(result.code).toBe(500); expect(result.message).toContain('Validation failed'); } }); it('should handle CustomEvents without detail property', () => { const handler = (0, safe_event_never_throws_1.safeCustomEvent)({ guard: isStockUpdate, }); const noDetailEvent = new CustomEvent('stock-update'); const result = handler(noDetailEvent); expect(result.status).toBe(safe_event_never_throws_1.Status.ERROR); if (result.status === safe_event_never_throws_1.Status.ERROR) { expect(result.code).toBe(500); expect(result.message).toContain('Validation failed'); } }); }); describe('Storage Events', () => { it('should handle StorageEvents with missing properties', () => { const handler = (0, safe_event_never_throws_1.safeStorageEvent)({ guard: (data) => { return (typeof data === 'object' && data !== null && typeof data.key === 'string'); }, }); const minimalEvent = new StorageEvent('storage'); const result = handler(minimalEvent); expect(result.status).toBe(safe_event_never_throws_1.Status.ERROR); if (result.status === safe_event_never_throws_1.Status.ERROR) { expect(result.code).toBe(500); expect(result.message).toContain('Validation failed'); } }); }); }); describe('Performance and Memory', () => { it('should handle large data objects efficiently', () => { const largeData = { id: 1, name: 'John', email: 'john@example.com', isActive: true, metadata: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tags: Array.from({ length: 1000 }, (_, i) => `tag-${i}`), history: Array.from({ length: 100 }, (_, i) => ({ action: `action-${i}`, timestamp: new Date().toISOString(), userId: i, })), }, }; const handler = (0, safe_event_never_throws_1.safePostMessage)({ guard: (data) => { return (typeof data === 'object' && data !== null && typeof data.id === 'number' && typeof data.name === 'string' && typeof data.email === 'string' && typeof data.isActive === 'boolean' && typeof data.metadata === 'object'); }, allowedOrigins: ['https://trusted-domain.com'], }); const event = new MessageEvent('message', { origin: 'https://trusted-domain.com', data: largeData, }); const startTime = performance.now(); const result = handler(event); const endTime = performance.now(); expect(result.status).toBe(safe_event_never_throws_1.Status.SUCCESS); expect(endTime - startTime).toBeLessThan(100); // Should complete within 100ms }); it('should handle rapid successive calls', () => { const handler = (0, safe_event_never_throws_1.safePostMessage)({ guard: isUserMessage, allowedOrigins: ['https://trusted-domain.com'], }); const events = Array.from({ length: 100 }, (_, i) => new MessageEvent('message', { origin: 'https://trusted-domain.com', data: { id: i, name: `User-${i}`, email: `user${i}@example.com`, isActive: true, }, })); const startTime = performance.now(); const results = events.map(event => handler(event)); const endTime = performance.now(); expect(results.every(result => result.status === safe_event_never_throws_1.Status.SUCCESS)).toBe(true); expect(endTime - startTime).toBeLessThan(1000); // Should complete within 1 second }); }); }); //# sourceMappingURL=error-handling.test.js.map