File size: 8,982 Bytes
4012011 d9879cf 4012011 d9879cf 4012011 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | 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<string, string> = { '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();
|