edtech / apps /api /src /scripts /test-e2e-journey.ts
CognxSafeTrack
chore: execute Sprint 38 technical debt resolution (Type Safety, Zod validation, Vitest, Mock LLM extracted)
d9879cf
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();