CognxSafeTrack commited on
Commit
4012011
·
1 Parent(s): e4e447f

test(e2e): add api journey tests for sprint 22 and 23 validations

Browse files
apps/api/src/scripts/test-e2e-journey.ts ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios';
2
+ import crypto from 'crypto';
3
+ import { PrismaClient } from '@repo/database';
4
+
5
+ const prisma = new PrismaClient();
6
+ const API_URL = process.env.API_URL || 'http://localhost:8080/v1';
7
+ const ADMIN_API_KEY = process.env.ADMIN_API_KEY || 'default_admin_key'; // Update accordingly
8
+ const WHATSAPP_APP_SECRET = process.env.WHATSAPP_APP_SECRET || ''; // If set, needed for webhook payload signing
9
+
10
+ const TEST_PHONE = '221770000000'; // Fake test number
11
+ const TEST_TRACK_ID = 'T1-business'; // Assuming T1 is Comprendre son business. We will fetch the actual T1 track.
12
+
13
+ // Helper to sign webhook payload
14
+ function generateSignature(payload: string): string {
15
+ if (!WHATSAPP_APP_SECRET) return '';
16
+ const hash = crypto.createHmac('sha256', WHATSAPP_APP_SECRET).update(payload).digest('hex');
17
+ return `sha256=${hash}`;
18
+ }
19
+
20
+ // Helper to simulate incoming WhatsApp Text
21
+ async function simulateWhatsAppMessage(phone: string, text: string) {
22
+ const payload = JSON.stringify({
23
+ object: 'whatsapp_business_account',
24
+ entry: [{
25
+ id: 'test_entry',
26
+ changes: [{
27
+ value: {
28
+ messaging_product: 'whatsapp',
29
+ metadata: { phone_number_id: 'test_phone_id' },
30
+ messages: [{
31
+ from: phone,
32
+ id: `msg_${Date.now()}`,
33
+ timestamp: Date.now().toString(),
34
+ type: 'text',
35
+ text: { body: text }
36
+ }]
37
+ },
38
+ field: 'messages'
39
+ }]
40
+ }]
41
+ });
42
+
43
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
44
+ const signature = generateSignature(payload);
45
+ if (signature) headers['x-hub-signature-256'] = signature;
46
+
47
+ try {
48
+ await axios.post(`${API_URL}/whatsapp/webhook`, payload, { headers });
49
+ console.log(`[E2E] Simulated WhatsApp Message from ${phone}: "${text}"`);
50
+ } catch (e: any) {
51
+ console.error(`[E2E] Failed to simulate message: ${e.response?.data?.error || e.message}`);
52
+ }
53
+ }
54
+
55
+ // Helper to sleep for async DB updates
56
+ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
57
+
58
+ async function runTests() {
59
+ console.log('🚀 Starting XAMLÉ E2E Journey Test (Sprints 22 & 23)');
60
+
61
+ try {
62
+ // 1. Cleanup previous test state
63
+ await prisma.userProgress.deleteMany({ where: { user: { phone: TEST_PHONE } } });
64
+ await prisma.response.deleteMany({ where: { user: { phone: TEST_PHONE } } });
65
+ await prisma.enrollment.deleteMany({ where: { user: { phone: TEST_PHONE } } });
66
+ await prisma.message.deleteMany({ where: { user: { phone: TEST_PHONE } } });
67
+ await prisma.businessProfile.deleteMany({ where: { user: { phone: TEST_PHONE } } });
68
+ await prisma.user.deleteMany({ where: { phone: TEST_PHONE } });
69
+ console.log('[E2E] Cleanup complete.');
70
+
71
+ // Initialize User directly for speed, or via onboarding.
72
+ // We'll create the user directly in DB to simulate the exact WOLOF Couture state
73
+ const track = await prisma.track.findFirst({ where: { title: { contains: 'Comprendre son business' } } });
74
+ if (!track) throw new Error("Could not find T1 Track in DB.");
75
+
76
+ const user = await prisma.user.create({
77
+ data: {
78
+ phone: TEST_PHONE,
79
+ language: 'WOLOF',
80
+ onboardingStatus: 'COMPLETED',
81
+ activity: 'Couture',
82
+ }
83
+ });
84
+
85
+ const enrollment = await prisma.enrollment.create({
86
+ data: {
87
+ userId: user.id,
88
+ trackId: track.id,
89
+ currentDay: 1,
90
+ status: 'ACTIVE'
91
+ }
92
+ });
93
+
94
+ console.log(`[E2E] Created Test User | ID: ${user.id} | Track: ${track.id} | CurrentDay: 1`);
95
+
96
+ // --- TEST 1: ANTI-SAUT ---
97
+ console.log('\n--- TEST 1: ANTI-SAUT ---');
98
+ // Simulate sending a response to Day 1
99
+ console.log(`[E2E] Sending Day 1 Answer...`);
100
+ await simulateWhatsAppMessage(TEST_PHONE, "Couture answer for day 1");
101
+ await sleep(3000); // give time for queue & worker to process AI eval
102
+
103
+ // User should normally receive Bravo and then reply 'SUITE'. Let's simulate 'SUITE'.
104
+ console.log(`[E2E] Sending 'SUITE' keyword...`);
105
+ await simulateWhatsAppMessage(TEST_PHONE, "SUITE");
106
+ await sleep(2000);
107
+
108
+ const updatedEnrollment = await prisma.enrollment.findFirst({ where: { userId: user.id, trackId: track.id } });
109
+ if (updatedEnrollment?.currentDay === 2) {
110
+ console.log(`✅ [Anti-Saut Test Passed] currentDay est exactement 2.`);
111
+ } else {
112
+ console.error(`❌ [Anti-Saut Test Failed] currentDay is ${updatedEnrollment?.currentDay} instead of 2.`);
113
+ }
114
+
115
+ // --- TEST 2: HUMAN-IN-THE-LOOP ---
116
+ console.log('\n--- TEST 2: HUMAN-IN-THE-LOOP (WOLOF AUDIO) ---');
117
+ // Since we can't easily mock an audio upload to Meta right now, we will manually insert a `PENDING_REVIEW`
118
+ // into the tracking flow. Actually, we can trigger the Worker indirectly, but inserting the userProgress
119
+ // to mimic Wolof interception is simpler for an E2E API test.
120
+
121
+ console.log(`[E2E] Simulating WOLOF Interception (Setting PENDING_REVIEW)...`);
122
+ await prisma.userProgress.upsert({
123
+ where: { userId_trackId: { userId: user.id, trackId: track.id } },
124
+ update: { exerciseStatus: 'PENDING_REVIEW' as any },
125
+ create: { userId: user.id, trackId: track.id, exerciseStatus: 'PENDING_REVIEW' as any }
126
+ });
127
+
128
+ const preReview = await prisma.userProgress.findUnique({ where: { userId_trackId: { userId: user.id, trackId: track.id } } });
129
+ if ((preReview?.exerciseStatus as string) === 'PENDING_REVIEW') {
130
+ console.log(`✅ [Human-in-the-Loop] Status correctly set to PENDING_REVIEW.`);
131
+ }
132
+
133
+ console.log(`[E2E] Admin calling POST /admin/override-feedback...`);
134
+ try {
135
+ const adminRes = await axios.post(`${API_URL}/admin/override-feedback`, {
136
+ userId: user.id,
137
+ trackId: track.id,
138
+ transcription: 'Traduction manuelle validée de la couture.',
139
+ overrideAudioUrl: 'https://fake-s3-url.com/audio.webm',
140
+ adminId: 'E2E_ADMIN'
141
+ }, {
142
+ headers: { 'Authorization': `Bearer ${ADMIN_API_KEY}` }
143
+ });
144
+ console.log(`[E2E] Admin Overdrive Response: ${adminRes.status}`);
145
+
146
+ // Wait for BullMQ to process the overdrive logic and send SUITE automatically
147
+ await sleep(4000);
148
+
149
+ const postReview = await prisma.userProgress.findUnique({ where: { userId_trackId: { userId: user.id, trackId: track.id } } });
150
+ const progressEnrollment = await prisma.enrollment.findFirst({ where: { userId: user.id, trackId: track.id } });
151
+
152
+ if ((postReview?.exerciseStatus as string) === 'COMPLETED' && progressEnrollment?.currentDay === 3) {
153
+ console.log(`✅ [Human-in-the-Loop Test Passed] User unlocked & progressed to Day 3.`);
154
+ } else {
155
+ console.log(`⚠️ [Human-in-the-Loop Note] exerciseStatus: ${postReview?.exerciseStatus}, currentDay: ${progressEnrollment?.currentDay}`);
156
+ }
157
+
158
+ } catch (e: any) {
159
+ console.error(`❌ [Human-in-the-Loop Test Failed] API Error: ${e.response?.data?.error || e.message}`);
160
+ }
161
+
162
+ // --- TEST 3: BADGE GUARD ---
163
+ console.log('\n--- TEST 3: BADGE GUARD VERIFICATION ---');
164
+ // Inject a REPRISE badge and force currentDay to 3.5 (Fractional)
165
+ await prisma.userProgress.update({
166
+ where: { userId_trackId: { userId: user.id, trackId: track.id } },
167
+ data: { badges: ['CLARTÉ', 'REPRISE'] }
168
+ });
169
+ await prisma.enrollment.update({
170
+ where: { id: enrollment.id },
171
+ data: { currentDay: 3.5 }
172
+ });
173
+
174
+ console.log(`[E2E] User has Badges: ['CLARTÉ', 'REPRISE'] on Day 3.5`);
175
+ // Simulating the pedagogy send logic. If we were to send SUITE now, it would increment to 4 and trigger pedagogy.ts.
176
+ console.log(`[E2E] Sending 'SUITE' to trigger Day 4 (which should NOT show REPRISE badge)...`);
177
+ await simulateWhatsAppMessage(TEST_PHONE, "SUITE");
178
+ await sleep(3000);
179
+
180
+ const finalEnrollment = await prisma.enrollment.findFirst({ where: { userId: user.id, trackId: track.id } });
181
+ console.log(`[E2E] Final Day is now: ${finalEnrollment?.currentDay}. Check server logs above for [Badge Guard] isVisible: false.`);
182
+
183
+ console.log('\n🎉 All specified E2E simulations completed.');
184
+ process.exit(0);
185
+
186
+ } catch (error) {
187
+ console.error('❌ E2E Test Execution Failed:', error);
188
+ process.exit(1);
189
+ }
190
+ }
191
+
192
+ runTests();