import axios from 'axios'; import crypto from 'crypto'; import { PrismaClient } from '@repo/database'; const prisma = new PrismaClient(); const API_URL = process.env.API_URL || 'http://localhost:8080/v1'; const ADMIN_API_KEY = process.env.ADMIN_API_KEY || 'default_admin_key'; // Update accordingly const WHATSAPP_APP_SECRET = process.env.WHATSAPP_APP_SECRET || ''; // If set, needed for webhook payload signing const TEST_PHONE = '221770000000'; // Fake test number // Helper to sign webhook payload function generateSignature(payload: string): string { if (!WHATSAPP_APP_SECRET) return ''; const hash = crypto.createHmac('sha256', WHATSAPP_APP_SECRET).update(payload).digest('hex'); return `sha256=${hash}`; } // Helper to simulate incoming WhatsApp Text async function simulateWhatsAppMessage(phone: string, text: string) { const payload = JSON.stringify({ object: 'whatsapp_business_account', entry: [{ id: 'test_entry', changes: [{ value: { messaging_product: 'whatsapp', metadata: { phone_number_id: 'test_phone_id' }, messages: [{ from: phone, id: `msg_${Date.now()}`, timestamp: Date.now().toString(), type: 'text', text: { body: text } }] }, field: 'messages' }] }] }); const headers: Record = { 'Content-Type': 'application/json' }; const signature = generateSignature(payload); if (signature) headers['x-hub-signature-256'] = signature; try { await axios.post(`${API_URL}/whatsapp/webhook`, payload, { headers }); console.log(`[E2E] Simulated WhatsApp Message from ${phone}: "${text}"`); } catch (e: unknown) { console.error(`[E2E] Failed to simulate message: ${(e as any).response?.data?.error || (e instanceof Error ? (e instanceof Error ? e.message : String(e)) : String(e))}`); } } // Helper to sleep for async DB updates const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); async function runTests() { console.log('🚀 Starting XAMLÉ E2E Journey Test (Sprints 22 & 23)'); try { // 1. Cleanup previous test state await prisma.userProgress.deleteMany({ where: { user: { phone: TEST_PHONE } } }); await prisma.response.deleteMany({ where: { user: { phone: TEST_PHONE } } }); await prisma.enrollment.deleteMany({ where: { user: { phone: TEST_PHONE } } }); await prisma.message.deleteMany({ where: { user: { phone: TEST_PHONE } } }); await prisma.businessProfile.deleteMany({ where: { user: { phone: TEST_PHONE } } }); await prisma.user.deleteMany({ where: { phone: TEST_PHONE } }); console.log('[E2E] Cleanup complete.'); // Initialize User directly for speed, or via onboarding. // We'll create the user directly in DB to simulate the exact WOLOF Couture state const track = await prisma.track.findFirst({ where: { title: { contains: 'Comprendre son business' } } }); if (!track) throw new Error("Could not find T1 Track in DB."); const user = await prisma.user.create({ data: { phone: TEST_PHONE, language: 'WOLOF', activity: 'Couture', } }); const enrollment = await prisma.enrollment.create({ data: { userId: user.id, trackId: track.id, currentDay: 1, status: 'ACTIVE' } }); console.log(`[E2E] Created Test User | ID: ${user.id} | Track: ${track.id} | CurrentDay: 1`); // --- TEST 1: ANTI-SAUT --- console.log('\n--- TEST 1: ANTI-SAUT ---'); // Simulate sending a response to Day 1 console.log(`[E2E] Sending Day 1 Answer...`); await simulateWhatsAppMessage(TEST_PHONE, "Couture answer for day 1"); await sleep(3000); // give time for queue & worker to process AI eval // User should normally receive Bravo and then reply 'SUITE'. Let's simulate 'SUITE'. console.log(`[E2E] Sending 'SUITE' keyword...`); await simulateWhatsAppMessage(TEST_PHONE, "SUITE"); await sleep(2000); const updatedEnrollment = await prisma.enrollment.findFirst({ where: { userId: user.id, trackId: track.id } }); if (updatedEnrollment?.currentDay === 2) { console.log(`✅ [Anti-Saut Test Passed] currentDay est exactement 2.`); } else { console.error(`❌ [Anti-Saut Test Failed] currentDay is ${updatedEnrollment?.currentDay} instead of 2.`); } // --- TEST 2: HUMAN-IN-THE-LOOP --- console.log('\n--- TEST 2: HUMAN-IN-THE-LOOP (WOLOF AUDIO) ---'); // Since we can't easily mock an audio upload to Meta right now, we will manually insert a `PENDING_REVIEW` // into the tracking flow. Actually, we can trigger the Worker indirectly, but inserting the userProgress // to mimic Wolof interception is simpler for an E2E API test. console.log(`[E2E] Simulating WOLOF Interception (Setting PENDING_REVIEW)...`); await prisma.userProgress.upsert({ where: { userId_trackId: { userId: user.id, trackId: track.id } }, update: { exerciseStatus: 'PENDING_REVIEW' as any }, create: { userId: user.id, trackId: track.id, exerciseStatus: 'PENDING_REVIEW' as any } }); const preReview = await prisma.userProgress.findUnique({ where: { userId_trackId: { userId: user.id, trackId: track.id } } }); if ((preReview?.exerciseStatus as string) === 'PENDING_REVIEW') { console.log(`✅ [Human-in-the-Loop] Status correctly set to PENDING_REVIEW.`); } console.log(`[E2E] Admin calling POST /admin/override-feedback...`); try { const adminRes = await axios.post(`${API_URL}/admin/override-feedback`, { userId: user.id, trackId: track.id, transcription: 'Traduction manuelle validée de la couture.', overrideAudioUrl: 'https://fake-s3-url.com/audio.webm', adminId: 'E2E_ADMIN' }, { headers: { 'Authorization': `Bearer ${ADMIN_API_KEY}` } }); console.log(`[E2E] Admin Overdrive Response: ${adminRes.status}`); // Wait for BullMQ to process the overdrive logic and send SUITE automatically await sleep(4000); const postReview = await prisma.userProgress.findUnique({ where: { userId_trackId: { userId: user.id, trackId: track.id } } }); const progressEnrollment = await prisma.enrollment.findFirst({ where: { userId: user.id, trackId: track.id } }); if ((postReview?.exerciseStatus as string) === 'COMPLETED' && progressEnrollment?.currentDay === 3) { console.log(`✅ [Human-in-the-Loop Test Passed] User unlocked & progressed to Day 3.`); } else { console.log(`⚠️ [Human-in-the-Loop Note] exerciseStatus: ${postReview?.exerciseStatus}, currentDay: ${progressEnrollment?.currentDay}`); } } catch (e: unknown) { console.error(`❌ [Human-in-the-Loop Test Failed] API Error: ${(e as any).response?.data?.error || (e instanceof Error ? (e instanceof Error ? e.message : String(e)) : String(e))}`); } // --- TEST 3: BADGE GUARD --- console.log('\n--- TEST 3: BADGE GUARD VERIFICATION ---'); // Inject a REPRISE badge and force currentDay to 3.5 (Fractional) await prisma.userProgress.update({ where: { userId_trackId: { userId: user.id, trackId: track.id } }, data: { badges: ['CLARTÉ', 'REPRISE'] } }); await prisma.enrollment.update({ where: { id: enrollment.id }, data: { currentDay: 3.5 } }); console.log(`[E2E] User has Badges: ['CLARTÉ', 'REPRISE'] on Day 3.5`); // Simulating the pedagogy send logic. If we were to send SUITE now, it would increment to 4 and trigger pedagogy.ts. console.log(`[E2E] Sending 'SUITE' to trigger Day 4 (which should NOT show REPRISE badge)...`); await simulateWhatsAppMessage(TEST_PHONE, "SUITE"); await sleep(3000); const finalEnrollment = await prisma.enrollment.findFirst({ where: { userId: user.id, trackId: track.id } }); console.log(`[E2E] Final Day is now: ${finalEnrollment?.currentDay}. Check server logs above for [Badge Guard] isVisible: false.`); console.log('\n🎉 All specified E2E simulations completed.'); process.exit(0); } catch (error) { console.error('❌ E2E Test Execution Failed:', error); process.exit(1); } } runTests();