editor.chain().focus().toggleBold().run()}
isActive={editor.isActive('bold')}
- title="Bold"
+ title="Bold (Ctrl+B)"
>
-
+
editor.chain().focus().toggleItalic().run()}
isActive={editor.isActive('italic')}
- title="Italic"
+ title="Italic (Ctrl+I)"
>
-
+
editor.chain().focus().toggleStrike().run()}
isActive={editor.isActive('strike')}
- title="Strike"
+ title="Strikethrough (Ctrl+U)"
>
-
+
= ({
isActive={editor.isActive('heading', { level: 1 })}
title="Heading 1"
>
-
+
editor.chain().focus().toggleHeading({ level: 2 }).run()}
isActive={editor.isActive('heading', { level: 2 })}
title="Heading 2"
>
-
+
= ({
isActive={editor.isActive('bulletList')}
title="Bullet List"
>
-
+
editor.chain().focus().toggleOrderedList().run()}
isActive={editor.isActive('orderedList')}
title="Ordered List"
>
-
+
= ({
isActive={editor.isActive('codeBlock')}
title="Code Block"
>
-
+
editor.chain().focus().toggleBlockquote().run()}
isActive={editor.isActive('blockquote')}
- title="Quote"
+ title="Blockquote"
>
-
+
@@ -145,28 +213,31 @@ export const RichContentEditor: React.FC
= ({
editor.chain().focus().undo().run()}
disabled={!editor.can().undo()}
- title="Undo"
+ title="Undo (Ctrl+Z)"
>
-
+
editor.chain().focus().redo().run()}
disabled={!editor.can().redo()}
- title="Redo"
+ title="Redo (Ctrl+Y)"
>
-
+
-
+
{/* Editor Content */}
diff --git a/src/services/errorReporting.ts b/src/services/errorReporting.ts
index e08b2f52..11b00a0d 100644
--- a/src/services/errorReporting.ts
+++ b/src/services/errorReporting.ts
@@ -4,6 +4,9 @@
*/
import { formatErrorForLogging } from '@/utils/errorUtils';
+import { createLogger } from '@/lib/logging';
+
+const logger = createLogger('errorReporting');
export interface ErrorReport {
id: string;
@@ -34,6 +37,9 @@ class ErrorReportingService {
this.sessionId = this.generateSessionId();
this.isProduction = typeof process !== 'undefined' && process.env?.NODE_ENV === 'production';
this.setupGlobalErrorHandlers();
+ logger.info('ErrorReportingService initialized', {
+ context: { sessionId: this.sessionId, isProduction: this.isProduction },
+ });
}
/**
@@ -93,10 +99,18 @@ class ErrorReportingService {
async reportError(error: any, context?: Record
): Promise {
const report = this.createErrorReport(error, context);
- // Log to console in development
- if (!this.isProduction) {
- console.error('Error Report:', report);
- }
+ // Log to structured logger
+ logger.error('Error reported', {
+ context: {
+ reportId: report.id,
+ errorType: report.errorData.type,
+ errorMessage: report.errorData.message,
+ userId: report.userId,
+ sessionId: report.sessionId,
+ ...context,
+ },
+ error,
+ });
// Send to error tracking service (e.g., Sentry, LogRocket)
if (this.isProduction) {
@@ -142,10 +156,12 @@ class ErrorReportingService {
});
if (!response.ok) {
- console.error('Failed to send error report:', response.statusText);
+ logger.warn('Failed to send error report', {
+ context: { status: response.status, statusText: response.statusText },
+ });
}
} catch (err) {
- console.error('Error sending error report:', err);
+ logger.error('Error sending error report', { error: err });
}
}
diff --git a/src/workers/__tests__/sms-cluster-worker.test.ts b/src/workers/__tests__/sms-cluster-worker.test.ts
index 8a1adc96..a4340225 100644
--- a/src/workers/__tests__/sms-cluster-worker.test.ts
+++ b/src/workers/__tests__/sms-cluster-worker.test.ts
@@ -1,5 +1,11 @@
import cluster from 'cluster';
-import { startSMSClusterWorker } from '../sms-cluster-worker';
+import {
+ startSMSClusterWorker,
+ sanitizeAndValidateSMS,
+ getSecurityEvents,
+ getQueueSize,
+ getRateLimitStatus
+} from '../sms-cluster-worker';
jest.mock('cluster', () => ({
isPrimary: true,
@@ -14,41 +20,172 @@ jest.mock('os', () => ({
describe('SMS Cluster Worker', () => {
beforeEach(() => {
jest.clearAllMocks();
+ // Clear security events before each test
+ const events = getSecurityEvents();
+ events.length = 0;
});
- it('should fork a worker for each CPU if primary', () => {
- // Override cluster.isPrimary just in case
- Object.defineProperty(cluster, 'isPrimary', { value: true, configurable: true });
+ describe('Cluster Management', () => {
+ it('should fork a worker for each CPU if primary', () => {
+ Object.defineProperty(cluster, 'isPrimary', { value: true, configurable: true });
- const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
- startSMSClusterWorker();
+ startSMSClusterWorker();
- expect(cluster.fork).toHaveBeenCalledTimes(4);
- expect(consoleSpy).toHaveBeenCalledWith(
- expect.stringContaining('Setting up cluster with 4 workers'),
- );
+ expect(cluster.fork).toHaveBeenCalledTimes(4);
+ expect(consoleSpy).toHaveBeenCalledWith(
+ expect.stringContaining('Setting up cluster with 4 workers'),
+ );
- consoleSpy.mockRestore();
+ consoleSpy.mockRestore();
+ });
+
+ it('should bind an exit handler to auto-heal workers', () => {
+ Object.defineProperty(cluster, 'isPrimary', { value: true, configurable: true });
+
+ startSMSClusterWorker();
+
+ expect(cluster.on).toHaveBeenCalledWith('exit', expect.any(Function));
+ });
+
+ it('should execute worker logic if not primary', () => {
+ Object.defineProperty(cluster, 'isPrimary', { value: false, configurable: true });
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
+
+ startSMSClusterWorker();
+
+ expect(cluster.fork).not.toHaveBeenCalled();
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Worker'));
+
+ consoleSpy.mockRestore();
+ });
+
+ it('should set worker resource limits', () => {
+ Object.defineProperty(cluster, 'isPrimary', { value: true, configurable: true });
+
+ startSMSClusterWorker();
+
+ expect(cluster.fork).toHaveBeenCalledWith(
+ expect.objectContaining({
+ WORKER_ID: expect.any(Number),
+ NODE_OPTIONS: expect.stringContaining('--max-old-space-size=512'),
+ })
+ );
+ });
});
- it('should bind an exit handler to auto-heal workers', () => {
- Object.defineProperty(cluster, 'isPrimary', { value: true, configurable: true });
+ describe('Input Validation', () => {
+ it('should validate correct phone numbers', () => {
+ const result = sanitizeAndValidateSMS('+15551234567', 'Test message');
+ expect(result.valid).toBe(true);
+ expect(result.sanitized).toBeDefined();
+ });
+
+ it('should reject invalid phone number format', () => {
+ const result = sanitizeAndValidateSMS('5551234567', 'Test message');
+ expect(result.valid).toBe(false);
+ expect(result.reason).toContain('Invalid phone number format');
+ });
- startSMSClusterWorker();
+ it('should reject phone numbers that are too long', () => {
+ const result = sanitizeAndValidateSMS('+155512345678901234567', 'Test message');
+ expect(result.valid).toBe(false);
+ expect(result.reason).toContain('too long');
+ });
- expect(cluster.on).toHaveBeenCalledWith('exit', expect.any(Function));
+ it('should reject non-string phone numbers', () => {
+ const result = sanitizeAndValidateSMS(1234567890 as any, 'Test message');
+ expect(result.valid).toBe(false);
+ expect(result.reason).toContain('must be a string');
+ });
+
+ it('should reject empty messages', () => {
+ const result = sanitizeAndValidateSMS('+15551234567', '');
+ expect(result.valid).toBe(false);
+ expect(result.reason).toContain('cannot be empty');
+ });
+
+ it('should reject messages that are too long', () => {
+ const longMessage = 'a'.repeat(2000);
+ const result = sanitizeAndValidateSMS('+15551234567', longMessage);
+ expect(result.valid).toBe(false);
+ expect(result.reason).toContain('too long');
+ });
+
+ it('should sanitize message content', () => {
+ const result = sanitizeAndValidateSMS('+15551234567', '');
+ expect(result.valid).toBe(true);
+ expect(result.sanitized?.message).not.toContain('