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();