import { describe, it, expect, beforeEach, vi } from 'vitest'; import { SMSClassifierService } from '../services/spamshield/spamshield.service'; // Mock shared-db before anything else (Prisma client is not generated in test env) vi.mock('@shieldsai/shared-db', () => ({ prisma: {}, SpamFeedback: {}, })); // Mock the feature flags module to control enableMLClassifier vi.mock('../services/spamshield/spamshield.config', () => ({ spamShieldEnv: { SPAM_THRESHOLD_AUTO_BLOCK: 0.85, SPAM_THRESHOLD_FLAG: 0.6, }, spamFeatureFlags: { enableMLClassifier: true, }, SpamDecision: { ALLOW: 'allow', FLAG: 'flag', BLOCK: 'block', CHALLENGE: 'challenge', }, SpamLayer: { NUMBER_REPUTATION: 'number_reputation', CONTENT_CLASSIFICATION: 'content_classification', BEHAVIORAL_ANALYSIS: 'behavioral_analysis', COMMUNITY_INTELLIGENCE: 'community_intelligence', }, ConfidenceLevel: { LOW: 'low', MEDIUM: 'medium', HIGH: 'high', VERY_HIGH: 'very_high', }, spamRateLimits: {}, })); describe('SMSClassifierService', () => { let classifier: SMSClassifierService; let initializeCalls: number; let initializeDelay: Promise; beforeEach(() => { // Re-import after mock to get fresh module state initializeCalls = 0; initializeDelay = new Promise(resolve => setTimeout(resolve, 50)); classifier = new SMSClassifierService(); // Override initialize to track calls and add delay classifier.initialize = async () => { initializeCalls++; await initializeDelay; }; }); describe('initialization race condition', () => { it('should call initialize only once under concurrent classify calls', async () => { const promises = Array.from({ length: 10 }, () => classifier.classify('ACT NOW - Limited offer!'), ); const results = await Promise.all(promises); expect(initializeCalls).toBe(1); expect(results).toHaveLength(10); results.forEach(r => { expect(r).toHaveProperty('isSpam'); expect(r).toHaveProperty('confidence'); expect(r).toHaveProperty('spamFeatures'); }); }); it('should handle interleaved calls after partial initialization', async () => { const batch1 = Array.from({ length: 5 }, () => classifier.classify('First batch message'), ); await Promise.all(batch1); expect(initializeCalls).toBe(1); const batch2 = Array.from({ length: 5 }, () => classifier.classify('Second batch message'), ); await Promise.all(batch2); // initialize should still only have been called once expect(initializeCalls).toBe(1); }); it('should return consistent results for same input under concurrency', async () => { const text = 'URGENT: Click http://example.com now!'; const promises = Array.from({ length: 20 }, () => classifier.classify(text), ); const results = await Promise.all(promises); const firstResult = results[0]; results.forEach((r, i) => { expect(r.isSpam).toBe(firstResult.isSpam); expect(r.confidence).toBe(firstResult.confidence); expect(r.spamFeatures).toEqual(firstResult.spamFeatures); }); }); it('should handle rapid sequential calls without re-initializing', async () => { for (let i = 0; i < 50; i++) { await classifier.classify(`Message ${i}`); } expect(initializeCalls).toBe(1); }); }); describe('feature extraction', () => { it('should detect URL presence', async () => { const result = await classifier.classify('Visit www.example.com'); expect(result.spamFeatures).toContain('url_present'); }); it('should detect urgency keywords', async () => { const result = await classifier.classify('Act now! This offer is urgent.'); expect(result.spamFeatures).toContain('urgency_keyword'); }); it('should detect excessive capitalization', async () => { const result = await classifier.classify('BUY THIS NOW!!!'); expect(result.spamFeatures).toContain('excessive_caps'); }); it('should detect multiple features', async () => { const result = await classifier.classify( 'URGENT: Visit www.example.com NOW!!!', ); expect(result.spamFeatures).toContain('url_present'); expect(result.spamFeatures).toContain('urgency_keyword'); expect(result.spamFeatures).toContain('excessive_caps'); }); }); });