+ const rowRenderer = useCallback(
+ ({ index, style }: { index: number; style: CSSProperties }) => {
+ const row = rows[index];
+ if (!row) return null;
+
+ if (row.type === 'header') {
+ return (
+
}
+ );
+ }
- {loading && activities.length > 0 && (
-
-
+ const activity = row.activity;
+ return (
+
+ {activity.actorAvatar ? (
+
+ ) : (
+
+ )}
+
+
+ {activity.actorName}{' '}
+ {activity.action}
+ {activity.targetTitle && (
+ <>
+ {' '}
+ {activity.targetTitle}
+ >
+ )}
+
+
+ {getRelativeTime(activity.createdAt)}
+
- )}
+
+ );
+ },
+ [rows],
+ );
- {!loading && !hasMore && activities.length > 0 && (
-
No more activity
- )}
+ const handleItemsRendered = useCallback(
+ ({ visibleStopIndex }: { visibleStopIndex: number }) => {
+ if (hasMore && !loading && visibleStopIndex >= rowCount - 5) {
+ loadMore();
+ }
+ },
+ [hasMore, loading, rowCount, loadMore],
+ );
- {!loading && activities.length === 0 && (
-
- No activity yet.
-
- )}
+ return (
+
+
+
Activity
+
+ {loading && activities.length === 0 && (
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+ )}
+
+ {!loading && activities.length === 0 && (
+
+ No activity yet.
+
+ )}
+
+ {activities.length > 0 && (
+
+
+ {({ height, width }: { height: number; width: number }) => (
+
+ {rowRenderer}
+
+ )}
+
+
+ )}
+
+ {loading && activities.length > 0 && (
+
+
+
+ )}
+
+ {!loading && !hasMore && activities.length > 0 && (
+
+ No more activity
+
+ )}
);
-}
+}
\ No newline at end of file
diff --git a/src/components/social/__tests__/socialFeatures.test.tsx b/src/components/social/__tests__/socialFeatures.test.tsx
index af8e5abd..1668acda 100644
--- a/src/components/social/__tests__/socialFeatures.test.tsx
+++ b/src/components/social/__tests__/socialFeatures.test.tsx
@@ -17,6 +17,29 @@ vi.mock('next/router', () => ({
useRouter: () => ({ push: vi.fn(), query: {} }),
}));
+// Mock react-window's VariableSizeList to render all children inline
+vi.mock('react-window', () => ({
+ VariableSizeList: vi.fn(({ children, height, width, itemCount, itemSize, overscanCount, onItemsRendered }) => {
+ // Call onItemsRendered to simulate the list rendering all items
+ if (onItemsRendered) {
+ onItemsRendered({ visibleStopIndex: itemCount - 1 });
+ }
+ // Render each item by calling the children render prop
+ const items: React.ReactNode[] = [];
+ for (let i = 0; i < itemCount; i++) {
+ const style = { height: typeof itemSize === 'function' ? itemSize(i) : itemSize, width, position: 'absolute' as const, top: 0, left: 0 };
+ items.push(children({ index: i, style }));
+ }
+ return
{items}
;
+ }),
+ FixedSizeList: vi.fn(() => null),
+}));
+
+// Mock react-virtualized-auto-sizer to provide non-zero dimensions
+vi.mock('react-virtualized-auto-sizer', () => ({
+ default: vi.fn(({ children }) => children({ height: 600, width: 400 })),
+}));
+
// ─── Imports after mocks ───────────────────────────────────────────────────────
import { apiClient } from '@/lib/api';
diff --git a/src/services/errorReporting.ts b/src/services/errorReporting.ts
index 9e8cbbaa..4e046ba7 100644
--- a/src/services/errorReporting.ts
+++ b/src/services/errorReporting.ts
@@ -6,7 +6,6 @@
import { formatErrorForLogging } from '@/utils/errorUtils';
import { createLogger } from '@/lib/logging';
-const logger = createLogger('error-reporting');
export interface ErrorReport {
id: string;
@@ -37,6 +36,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 },
+ });
}
/**
@@ -96,10 +98,6 @@ class ErrorReportingService {
async reportError(error: any, context?: Record
): Promise {
const report = this.createErrorReport(error, context);
- // Log to console in development
- if (!this.isProduction) {
- logger.error('Error Report', { error: report });
- }
// Send to error tracking service (e.g., Sentry, LogRocket)
if (this.isProduction) {
@@ -145,7 +143,7 @@ class ErrorReportingService {
});
if (!response.ok) {
- logger.error('Failed to send error report', { status: response.statusText });
+
}
} catch (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('