@growsober/sdk 1.0.5 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/e2e.test.d.ts +30 -0
- package/dist/__tests__/e2e.test.js +959 -63
- package/dist/api/mutations/badges.d.ts +116 -0
- package/dist/api/mutations/badges.js +177 -0
- package/dist/api/mutations/brands.d.ts +251 -0
- package/dist/api/mutations/brands.js +242 -0
- package/dist/api/mutations/creators.d.ts +131 -0
- package/dist/api/mutations/creators.js +129 -0
- package/dist/api/mutations/event-chat.d.ts +2 -2
- package/dist/api/mutations/event-chat.js +9 -9
- package/dist/api/mutations/index.d.ts +4 -0
- package/dist/api/mutations/index.js +5 -1
- package/dist/api/mutations/jack.d.ts +29 -0
- package/dist/api/mutations/jack.js +41 -1
- package/dist/api/mutations/products.d.ts +175 -0
- package/dist/api/mutations/products.js +226 -0
- package/dist/api/mutations/support.d.ts +20 -1
- package/dist/api/mutations/support.js +36 -1
- package/dist/api/queries/badges.d.ts +221 -0
- package/dist/api/queries/badges.js +290 -0
- package/dist/api/queries/bookings.d.ts +1 -1
- package/dist/api/queries/brands.d.ts +248 -0
- package/dist/api/queries/brands.js +226 -0
- package/dist/api/queries/businesses.d.ts +61 -1
- package/dist/api/queries/businesses.js +27 -1
- package/dist/api/queries/creators.d.ts +332 -0
- package/dist/api/queries/creators.js +249 -0
- package/dist/api/queries/event-chat.d.ts +1 -1
- package/dist/api/queries/event-chat.js +4 -4
- package/dist/api/queries/events.d.ts +45 -0
- package/dist/api/queries/index.d.ts +5 -0
- package/dist/api/queries/index.js +6 -1
- package/dist/api/queries/jack.d.ts +80 -0
- package/dist/api/queries/jack.js +98 -1
- package/dist/api/queries/library.d.ts +8 -0
- package/dist/api/queries/products.d.ts +185 -0
- package/dist/api/queries/products.js +203 -0
- package/dist/api/queries/support.d.ts +46 -1
- package/dist/api/queries/support.js +48 -1
- package/dist/api/queries/venues.d.ts +304 -0
- package/dist/api/queries/venues.js +211 -0
- package/dist/api/types.d.ts +245 -0
- package/dist/api/types.js +6 -1
- package/dist/api/utils/eventGrouping.d.ts +104 -0
- package/dist/api/utils/eventGrouping.js +155 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -1
- package/package.json +5 -2
- package/src/__tests__/e2e.test.ts +996 -64
- package/src/api/mutations/badges.ts +228 -0
- package/src/api/mutations/brands.ts +376 -0
- package/src/api/mutations/creators.ts +171 -0
- package/src/api/mutations/event-chat.ts +8 -8
- package/src/api/mutations/index.ts +4 -0
- package/src/api/mutations/jack.ts +50 -1
- package/src/api/mutations/products.ts +336 -0
- package/src/api/mutations/support.ts +44 -0
- package/src/api/queries/badges.ts +385 -0
- package/src/api/queries/brands.ts +281 -0
- package/src/api/queries/businesses.ts +30 -1
- package/src/api/queries/creators.ts +308 -0
- package/src/api/queries/event-chat.ts +3 -3
- package/src/api/queries/index.ts +5 -0
- package/src/api/queries/jack.ts +139 -1
- package/src/api/queries/products.ts +312 -0
- package/src/api/queries/support.ts +54 -0
- package/src/api/queries/venues.ts +271 -0
- package/src/api/types.ts +317 -1
- package/src/api/utils/eventGrouping.ts +181 -0
- package/src/index.ts +6 -0
|
@@ -3,11 +3,41 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Tests all SDK endpoints against the real API.
|
|
5
5
|
* Run with: npx ts-node src/__tests__/e2e.test.ts
|
|
6
|
+
*
|
|
7
|
+
* Covers:
|
|
8
|
+
* - Authentication
|
|
9
|
+
* - Users
|
|
10
|
+
* - Events (with venue, creator, brand sponsor)
|
|
11
|
+
* - Bookings
|
|
12
|
+
* - Event Chat (new roles: ATTENDEE, CREATOR, VENUE_REP, BRAND_REP)
|
|
13
|
+
* - Hubs
|
|
14
|
+
* - Venues (new - replaces businesses)
|
|
15
|
+
* - Creators (new)
|
|
16
|
+
* - Brands (new)
|
|
17
|
+
* - Unified Rewards (venue + brand rewards)
|
|
18
|
+
* - Library (content workflow)
|
|
19
|
+
* - Support (check-ins, wins, Jack AI)
|
|
20
|
+
* - Creator Products (sessions/packages with booking flow)
|
|
21
|
+
* - Badges & Rewards
|
|
22
|
+
* - Badge Awarding (business logic tests for automatic badge awarding)
|
|
23
|
+
* - CHECK_IN_STREAK badge on check-in
|
|
24
|
+
* - WINS_LOGGED badge on logging wins
|
|
25
|
+
* - MOOD_LOGS_COUNT badge on mood logging
|
|
26
|
+
* - REFLECTIONS_COUNT badge on submitting reflections
|
|
27
|
+
* - EVENTS_HOSTED badge on publishing events
|
|
28
|
+
* - EVENTS_ATTENDED badge progress tracking
|
|
29
|
+
* - Reward Flow (full redemption lifecycle)
|
|
30
|
+
* - Creating rewards for badges (venue/brand)
|
|
31
|
+
* - User earns badge -> reward becomes available
|
|
32
|
+
* - Redeem reward (get redemption details)
|
|
33
|
+
* - Cannot redeem without badge
|
|
34
|
+
* - Cannot redeem twice (per user limit)
|
|
35
|
+
* - Reward verification endpoint
|
|
6
36
|
*/
|
|
7
37
|
|
|
8
38
|
import axios, { AxiosInstance } from 'axios';
|
|
9
39
|
|
|
10
|
-
const BASE_URL = 'http://localhost:3001/api/v1';
|
|
40
|
+
const BASE_URL = process.env.API_URL || 'http://localhost:3001/api/v1';
|
|
11
41
|
const TEST_EMAIL = `e2e-${Date.now()}@growsober.com`;
|
|
12
42
|
const TEST_PASSWORD = 'SecurePass123!';
|
|
13
43
|
|
|
@@ -19,6 +49,7 @@ const c = {
|
|
|
19
49
|
yellow: '\x1b[33m',
|
|
20
50
|
cyan: '\x1b[36m',
|
|
21
51
|
dim: '\x1b[2m',
|
|
52
|
+
bold: '\x1b[1m',
|
|
22
53
|
};
|
|
23
54
|
|
|
24
55
|
// State shared across tests
|
|
@@ -31,6 +62,20 @@ const state: {
|
|
|
31
62
|
bookingId?: string;
|
|
32
63
|
chatId?: string;
|
|
33
64
|
conversationId?: string;
|
|
65
|
+
// New state for restructured models
|
|
66
|
+
venueId?: string;
|
|
67
|
+
creatorId?: string;
|
|
68
|
+
brandId?: string;
|
|
69
|
+
availabilityId?: string;
|
|
70
|
+
rewardId?: string;
|
|
71
|
+
badgeId?: string;
|
|
72
|
+
contentId?: string;
|
|
73
|
+
// Creator products
|
|
74
|
+
productId?: string;
|
|
75
|
+
productBookingId?: string;
|
|
76
|
+
// Second user for ownership tests
|
|
77
|
+
secondUserToken?: string;
|
|
78
|
+
secondUserId?: string;
|
|
34
79
|
} = {
|
|
35
80
|
accessToken: '',
|
|
36
81
|
refreshToken: '',
|
|
@@ -40,6 +85,7 @@ const state: {
|
|
|
40
85
|
// Stats
|
|
41
86
|
let passed = 0;
|
|
42
87
|
let failed = 0;
|
|
88
|
+
let skipped = 0;
|
|
43
89
|
|
|
44
90
|
// Create axios client
|
|
45
91
|
function api(token?: string): AxiosInstance {
|
|
@@ -66,9 +112,20 @@ async function test(name: string, fn: () => Promise<void>): Promise<boolean> {
|
|
|
66
112
|
}
|
|
67
113
|
}
|
|
68
114
|
|
|
69
|
-
//
|
|
70
|
-
function
|
|
71
|
-
|
|
115
|
+
// Skip test with reason
|
|
116
|
+
function skip(name: string, reason: string): void {
|
|
117
|
+
console.log(`${c.yellow}SKIP${c.reset} ${name} ${c.dim}(${reason})${c.reset}`);
|
|
118
|
+
skipped++;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Unwrap API response (handles both wrapped and unwrapped)
|
|
122
|
+
function unwrap<T>(res: { data: any }): T {
|
|
123
|
+
return res.data?.data !== undefined ? res.data.data : res.data;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Section header
|
|
127
|
+
function section(name: string): void {
|
|
128
|
+
console.log(`\n${c.cyan}--- ${name} ---${c.reset}\n`);
|
|
72
129
|
}
|
|
73
130
|
|
|
74
131
|
// ============================================================================
|
|
@@ -76,16 +133,16 @@ function unwrap<T>(res: { data: { data: T } }): T {
|
|
|
76
133
|
// ============================================================================
|
|
77
134
|
|
|
78
135
|
async function runTests() {
|
|
79
|
-
console.log(`\n${c.cyan}${'='.repeat(
|
|
80
|
-
console.log(`${c.cyan} GrowSober API E2E Tests${c.reset}`);
|
|
81
|
-
console.log(`${c.cyan}${'='.repeat(
|
|
136
|
+
console.log(`\n${c.cyan}${'='.repeat(70)}${c.reset}`);
|
|
137
|
+
console.log(`${c.cyan} GrowSober API E2E Tests - Full Coverage${c.reset}`);
|
|
138
|
+
console.log(`${c.cyan}${'='.repeat(70)}${c.reset}`);
|
|
82
139
|
console.log(`${c.dim}Base URL: ${BASE_URL}${c.reset}`);
|
|
83
140
|
console.log(`${c.dim}Test Email: ${TEST_EMAIL}${c.reset}\n`);
|
|
84
141
|
|
|
85
142
|
// Health check
|
|
86
143
|
const health = await api().get('/health');
|
|
87
144
|
if (health.status !== 200) {
|
|
88
|
-
console.log(`${c.red}API is not reachable${c.reset}`);
|
|
145
|
+
console.log(`${c.red}API is not reachable at ${BASE_URL}${c.reset}`);
|
|
89
146
|
process.exit(1);
|
|
90
147
|
}
|
|
91
148
|
console.log(`${c.green}API is healthy${c.reset}\n`);
|
|
@@ -93,7 +150,7 @@ async function runTests() {
|
|
|
93
150
|
// =========================================================================
|
|
94
151
|
// AUTH
|
|
95
152
|
// =========================================================================
|
|
96
|
-
|
|
153
|
+
section('AUTHENTICATION');
|
|
97
154
|
|
|
98
155
|
await test('Register new user', async () => {
|
|
99
156
|
const res = await api().post('/auth/register', {
|
|
@@ -142,10 +199,23 @@ async function runTests() {
|
|
|
142
199
|
if (res.status !== 401) throw new Error(`Expected 401, got ${res.status}`);
|
|
143
200
|
});
|
|
144
201
|
|
|
202
|
+
// Register second user for ownership tests
|
|
203
|
+
await test('Register second user for ownership tests', async () => {
|
|
204
|
+
const res = await api().post('/auth/register', {
|
|
205
|
+
email: `e2e-second-${Date.now()}@growsober.com`,
|
|
206
|
+
password: TEST_PASSWORD,
|
|
207
|
+
name: 'E2E Second User',
|
|
208
|
+
});
|
|
209
|
+
if (res.status !== 201) throw new Error(`Status ${res.status}`);
|
|
210
|
+
const data = unwrap<any>(res);
|
|
211
|
+
state.secondUserToken = data.accessToken;
|
|
212
|
+
state.secondUserId = data.user.id;
|
|
213
|
+
});
|
|
214
|
+
|
|
145
215
|
// =========================================================================
|
|
146
216
|
// USERS
|
|
147
217
|
// =========================================================================
|
|
148
|
-
|
|
218
|
+
section('USERS');
|
|
149
219
|
|
|
150
220
|
await test('Get current user profile', async () => {
|
|
151
221
|
const res = await api(state.accessToken).get('/users/me');
|
|
@@ -171,9 +241,416 @@ async function runTests() {
|
|
|
171
241
|
});
|
|
172
242
|
|
|
173
243
|
// =========================================================================
|
|
174
|
-
//
|
|
244
|
+
// VENUES (New - replaces Businesses)
|
|
245
|
+
// =========================================================================
|
|
246
|
+
section('VENUES');
|
|
247
|
+
|
|
248
|
+
await test('List venues', async () => {
|
|
249
|
+
const res = await api().get('/venues');
|
|
250
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
251
|
+
const data = unwrap<any>(res);
|
|
252
|
+
// Get first venue if exists for further tests
|
|
253
|
+
if (data?.venues?.length > 0) {
|
|
254
|
+
state.venueId = data.venues[0].id;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await test('Get featured venues', async () => {
|
|
259
|
+
const res = await api().get('/venues/featured');
|
|
260
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await test('Get nearby venues', async () => {
|
|
264
|
+
const res = await api().get('/venues/nearby', {
|
|
265
|
+
params: { lat: 51.5074, long: -0.1278, radius: 10 },
|
|
266
|
+
});
|
|
267
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (state.venueId) {
|
|
271
|
+
await test('Get venue by ID', async () => {
|
|
272
|
+
const res = await api().get(`/venues/${state.venueId}`);
|
|
273
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await test('Get venue owners', async () => {
|
|
277
|
+
const res = await api(state.accessToken).get(`/venues/${state.venueId}/owners`);
|
|
278
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
await test('Get venue events', async () => {
|
|
282
|
+
const res = await api().get(`/venues/${state.venueId}/events`);
|
|
283
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
284
|
+
});
|
|
285
|
+
} else {
|
|
286
|
+
skip('Get venue by ID', 'No venues in database');
|
|
287
|
+
skip('Get venue owners', 'No venues in database');
|
|
288
|
+
skip('Get venue events', 'No venues in database');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// =========================================================================
|
|
292
|
+
// CREATORS (New)
|
|
293
|
+
// =========================================================================
|
|
294
|
+
section('CREATORS');
|
|
295
|
+
|
|
296
|
+
await test('List creators', async () => {
|
|
297
|
+
const res = await api().get('/creators');
|
|
298
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
await test('Create creator profile', async () => {
|
|
302
|
+
const res = await api(state.accessToken).post('/creators', {
|
|
303
|
+
slug: `e2e-creator-${Date.now()}`,
|
|
304
|
+
displayName: 'E2E Test Creator',
|
|
305
|
+
bio: 'A test creator for E2E tests',
|
|
306
|
+
canFacilitate: true,
|
|
307
|
+
canCreateContent: true,
|
|
308
|
+
isInfluencer: false,
|
|
309
|
+
specialties: ['meditation', 'sobriety-coaching'],
|
|
310
|
+
certifications: ['Test Cert 1'],
|
|
311
|
+
});
|
|
312
|
+
if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
313
|
+
const data = unwrap<any>(res);
|
|
314
|
+
state.creatorId = data.id;
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (state.creatorId) {
|
|
318
|
+
await test('Get creator by ID', async () => {
|
|
319
|
+
const res = await api().get(`/creators/${state.creatorId}`);
|
|
320
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await test('Get my creator profile', async () => {
|
|
324
|
+
const res = await api(state.accessToken).get('/creators/me');
|
|
325
|
+
// Returns 200 with null or creator data
|
|
326
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
await test('Get creator by user ID', async () => {
|
|
330
|
+
const res = await api(state.accessToken).get(`/creators/user/${state.userId}`);
|
|
331
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
await test('Update creator profile', async () => {
|
|
335
|
+
const res = await api(state.accessToken).patch(`/creators/${state.creatorId}`, {
|
|
336
|
+
bio: 'Updated bio for E2E testing',
|
|
337
|
+
sessionRate: 50,
|
|
338
|
+
});
|
|
339
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await test('Add creator availability', async () => {
|
|
343
|
+
const res = await api(state.accessToken).post(`/creators/${state.creatorId}/availability`, {
|
|
344
|
+
dayOfWeek: 1, // Monday
|
|
345
|
+
startTime: '09:00',
|
|
346
|
+
endTime: '17:00',
|
|
347
|
+
timezone: 'Europe/London',
|
|
348
|
+
isRecurring: true,
|
|
349
|
+
});
|
|
350
|
+
if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
351
|
+
const data = unwrap<any>(res);
|
|
352
|
+
state.availabilityId = data.id;
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await test('Get creator availability', async () => {
|
|
356
|
+
const res = await api().get(`/creators/${state.creatorId}/availability`);
|
|
357
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
await test('Get creator content (published)', async () => {
|
|
361
|
+
const res = await api().get(`/creators/${state.creatorId}/content`);
|
|
362
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
await test('Get creator events', async () => {
|
|
366
|
+
const res = await api().get(`/creators/${state.creatorId}/events`);
|
|
367
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Test ownership - second user cannot update
|
|
371
|
+
await test('Second user cannot update another creator profile (403)', async () => {
|
|
372
|
+
const res = await api(state.secondUserToken).patch(`/creators/${state.creatorId}`, {
|
|
373
|
+
bio: 'Hacked bio',
|
|
374
|
+
});
|
|
375
|
+
if (res.status !== 403) throw new Error(`Expected 403, got ${res.status}`);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// =========================================================================
|
|
380
|
+
// CREATOR PRODUCTS (Sessions/Packages)
|
|
381
|
+
// =========================================================================
|
|
382
|
+
section('CREATOR PRODUCTS');
|
|
383
|
+
|
|
384
|
+
if (state.creatorId) {
|
|
385
|
+
await test('Create creator product (1-on-1 session)', async () => {
|
|
386
|
+
const res = await api(state.accessToken).post(`/creators/${state.creatorId}/products`, {
|
|
387
|
+
title: 'E2E Test 1-on-1 Session',
|
|
388
|
+
description: 'A test coaching session for E2E testing',
|
|
389
|
+
type: 'SESSION_1ON1',
|
|
390
|
+
price: 5000, // £50 in pence
|
|
391
|
+
currency: 'GBP',
|
|
392
|
+
durationMinutes: 60,
|
|
393
|
+
maxParticipants: 1,
|
|
394
|
+
deliveryMethod: 'VIDEO_CALL',
|
|
395
|
+
});
|
|
396
|
+
if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
397
|
+
const data = unwrap<any>(res);
|
|
398
|
+
state.productId = data.id;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
await test('Create second product (workshop)', async () => {
|
|
402
|
+
const res = await api(state.accessToken).post(`/creators/${state.creatorId}/products`, {
|
|
403
|
+
title: 'E2E Test Workshop',
|
|
404
|
+
description: 'A group workshop for E2E testing',
|
|
405
|
+
type: 'WORKSHOP',
|
|
406
|
+
price: 2500, // £25 in pence
|
|
407
|
+
currency: 'GBP',
|
|
408
|
+
durationMinutes: 90,
|
|
409
|
+
maxParticipants: 10,
|
|
410
|
+
deliveryMethod: 'VIDEO_CALL',
|
|
411
|
+
});
|
|
412
|
+
if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
await test('Get creator products', async () => {
|
|
416
|
+
const res = await api(state.accessToken).get(`/creators/${state.creatorId}/products`);
|
|
417
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
418
|
+
const data = unwrap<any>(res);
|
|
419
|
+
const products = data.products || data;
|
|
420
|
+
if (!Array.isArray(products) || products.length < 2) {
|
|
421
|
+
throw new Error('Expected at least 2 products for creator');
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
if (state.productId) {
|
|
426
|
+
await test('Get product by ID (public)', async () => {
|
|
427
|
+
const res = await api().get(`/products/${state.productId}`);
|
|
428
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
429
|
+
const data = unwrap<any>(res);
|
|
430
|
+
if (!data.title || !data.price) throw new Error('Missing expected product fields');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
await test('Update product', async () => {
|
|
434
|
+
const res = await api(state.accessToken).put(`/creators/${state.creatorId}/products/${state.productId}`, {
|
|
435
|
+
price: 6000, // Update to £60
|
|
436
|
+
description: 'Updated description for E2E testing',
|
|
437
|
+
});
|
|
438
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
439
|
+
const data = unwrap<any>(res);
|
|
440
|
+
if (data.price !== 6000) throw new Error('Price not updated correctly');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Second user books the product
|
|
444
|
+
await test('Book product (as second user)', async () => {
|
|
445
|
+
const scheduledAt = new Date();
|
|
446
|
+
scheduledAt.setDate(scheduledAt.getDate() + 14); // 2 weeks from now
|
|
447
|
+
scheduledAt.setHours(10, 0, 0, 0);
|
|
448
|
+
|
|
449
|
+
const res = await api(state.secondUserToken).post(`/products/${state.productId}/book`, {
|
|
450
|
+
scheduledAt: scheduledAt.toISOString(),
|
|
451
|
+
timezone: 'Europe/London',
|
|
452
|
+
clientNotes: 'E2E test booking',
|
|
453
|
+
});
|
|
454
|
+
if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
455
|
+
const data = unwrap<any>(res);
|
|
456
|
+
state.productBookingId = data.id;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (state.productBookingId) {
|
|
460
|
+
await test('Get product booking details', async () => {
|
|
461
|
+
const res = await api(state.secondUserToken).get(`/users/me/product-bookings/${state.productBookingId}`);
|
|
462
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
463
|
+
const data = unwrap<any>(res);
|
|
464
|
+
if (!data.scheduledAt || !data.status) throw new Error('Missing booking fields');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
await test('Get user product bookings', async () => {
|
|
468
|
+
const res = await api(state.secondUserToken).get('/users/me/product-bookings');
|
|
469
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
470
|
+
const data = unwrap<any>(res);
|
|
471
|
+
const bookings = Array.isArray(data) ? data : (data.data || []);
|
|
472
|
+
if (!Array.isArray(bookings) || bookings.length === 0) {
|
|
473
|
+
throw new Error('Expected at least one product booking');
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
await test('Get creator bookings (as creator)', async () => {
|
|
478
|
+
const res = await api(state.accessToken).get(`/creators/${state.creatorId}/products/bookings`);
|
|
479
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
480
|
+
const data = unwrap<any>(res);
|
|
481
|
+
const bookings = Array.isArray(data) ? data : (data.data || []);
|
|
482
|
+
if (!Array.isArray(bookings) || bookings.length === 0) {
|
|
483
|
+
throw new Error('Expected at least one booking for creator products');
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
await test('Update product booking', async () => {
|
|
488
|
+
const newScheduledAt = new Date();
|
|
489
|
+
newScheduledAt.setDate(newScheduledAt.getDate() + 21); // 3 weeks from now
|
|
490
|
+
newScheduledAt.setHours(14, 0, 0, 0);
|
|
491
|
+
|
|
492
|
+
const res = await api(state.secondUserToken).put(`/users/me/product-bookings/${state.productBookingId}`, {
|
|
493
|
+
scheduledAt: newScheduledAt.toISOString(),
|
|
494
|
+
clientNotes: 'Rescheduled - E2E test',
|
|
495
|
+
});
|
|
496
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
await test('Cancel product booking', async () => {
|
|
500
|
+
const res = await api(state.secondUserToken).delete(`/users/me/product-bookings/${state.productBookingId}`, {
|
|
501
|
+
data: { reason: 'E2E test cleanup' },
|
|
502
|
+
});
|
|
503
|
+
if (res.status !== 200 && res.status !== 204) throw new Error(`Status ${res.status}`);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Second user cannot delete creator's product
|
|
508
|
+
await test('Second user cannot delete product (403)', async () => {
|
|
509
|
+
const res = await api(state.secondUserToken).delete(`/creators/${state.creatorId}/products/${state.productId}`);
|
|
510
|
+
if (res.status !== 403) throw new Error(`Expected 403, got ${res.status}`);
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
skip('Create creator product', 'No creator ID available');
|
|
515
|
+
skip('Create second product', 'No creator ID available');
|
|
516
|
+
skip('Get creator products', 'No creator ID available');
|
|
517
|
+
skip('Get product by ID', 'No product ID available');
|
|
518
|
+
skip('Update product', 'No product ID available');
|
|
519
|
+
skip('Book product', 'No product ID available');
|
|
520
|
+
skip('Get product booking details', 'No booking ID available');
|
|
521
|
+
skip('Get user product bookings', 'No booking ID available');
|
|
522
|
+
skip('Get creator bookings', 'No booking ID available');
|
|
523
|
+
skip('Update product booking', 'No booking ID available');
|
|
524
|
+
skip('Cancel product booking', 'No booking ID available');
|
|
525
|
+
skip('Second user cannot delete product', 'No product ID available');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Test public products listing
|
|
529
|
+
await test('List all public products', async () => {
|
|
530
|
+
const res = await api().get('/products');
|
|
531
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
532
|
+
const data = unwrap<any>(res);
|
|
533
|
+
// May have products array or paginated response
|
|
534
|
+
const products = data.products || data;
|
|
535
|
+
if (!Array.isArray(products)) throw new Error('Expected products array');
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// =========================================================================
|
|
539
|
+
// BRANDS (New)
|
|
540
|
+
// =========================================================================
|
|
541
|
+
section('BRANDS');
|
|
542
|
+
|
|
543
|
+
await test('List brands', async () => {
|
|
544
|
+
const res = await api().get('/brands');
|
|
545
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
await test('Get featured brands', async () => {
|
|
549
|
+
const res = await api().get('/brands/featured');
|
|
550
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
await test('Create brand', async () => {
|
|
554
|
+
const res = await api(state.accessToken).post('/brands', {
|
|
555
|
+
slug: `e2e-brand-${Date.now()}`,
|
|
556
|
+
name: 'E2E Test Brand',
|
|
557
|
+
description: 'A test brand for E2E testing',
|
|
558
|
+
website: 'https://example.com',
|
|
559
|
+
email: 'brand@example.com',
|
|
560
|
+
});
|
|
561
|
+
if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
562
|
+
const data = unwrap<any>(res);
|
|
563
|
+
state.brandId = data.id;
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
if (state.brandId) {
|
|
567
|
+
await test('Get brand by ID', async () => {
|
|
568
|
+
const res = await api().get(`/brands/${state.brandId}`);
|
|
569
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
await test('Update brand', async () => {
|
|
573
|
+
const res = await api(state.accessToken).patch(`/brands/${state.brandId}`, {
|
|
574
|
+
description: 'Updated brand description',
|
|
575
|
+
});
|
|
576
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
await test('Get brand owners', async () => {
|
|
580
|
+
const res = await api(state.accessToken).get(`/brands/${state.brandId}/owners`);
|
|
581
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// Add second user as brand manager
|
|
585
|
+
await test('Add brand owner (manager role)', async () => {
|
|
586
|
+
const res = await api(state.accessToken).post(`/brands/${state.brandId}/owners`, {
|
|
587
|
+
userId: state.secondUserId,
|
|
588
|
+
role: 'MANAGER',
|
|
589
|
+
});
|
|
590
|
+
if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// Manager can update brand
|
|
594
|
+
await test('Manager can update brand', async () => {
|
|
595
|
+
const res = await api(state.secondUserToken).patch(`/brands/${state.brandId}`, {
|
|
596
|
+
description: 'Updated by manager',
|
|
597
|
+
});
|
|
598
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
await test('Get brand creators (partnerships)', async () => {
|
|
602
|
+
const res = await api(state.accessToken).get(`/brands/${state.brandId}/creators`);
|
|
603
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// Add creator partnership if creator exists
|
|
607
|
+
if (state.creatorId) {
|
|
608
|
+
await test('Add creator partnership to brand', async () => {
|
|
609
|
+
const res = await api(state.accessToken).post(`/brands/${state.brandId}/creators`, {
|
|
610
|
+
creatorId: state.creatorId,
|
|
611
|
+
commissionRate: 0.15,
|
|
612
|
+
isActive: true,
|
|
613
|
+
});
|
|
614
|
+
if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
await test('Update creator partnership', async () => {
|
|
618
|
+
const res = await api(state.accessToken).patch(
|
|
619
|
+
`/brands/${state.brandId}/creators/${state.creatorId}`,
|
|
620
|
+
{
|
|
621
|
+
commissionRate: 0.20,
|
|
622
|
+
}
|
|
623
|
+
);
|
|
624
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
await test('Get brand sponsored events', async () => {
|
|
629
|
+
const res = await api().get(`/brands/${state.brandId}/events`);
|
|
630
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// Remove the manager
|
|
634
|
+
await test('Remove brand owner', async () => {
|
|
635
|
+
const res = await api(state.accessToken).delete(
|
|
636
|
+
`/brands/${state.brandId}/owners/${state.secondUserId}`
|
|
637
|
+
);
|
|
638
|
+
if (res.status !== 200 && res.status !== 204) throw new Error(`Status ${res.status}`);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// Second user should now be forbidden
|
|
642
|
+
await test('Removed owner cannot access brand (403)', async () => {
|
|
643
|
+
const res = await api(state.secondUserToken).patch(`/brands/${state.brandId}`, {
|
|
644
|
+
description: 'Should fail',
|
|
645
|
+
});
|
|
646
|
+
if (res.status !== 403) throw new Error(`Expected 403, got ${res.status}`);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// =========================================================================
|
|
651
|
+
// EVENTS (with new fields: venueId, creatorId, sponsorBrandId, gatheringType)
|
|
175
652
|
// =========================================================================
|
|
176
|
-
|
|
653
|
+
section('EVENTS');
|
|
177
654
|
|
|
178
655
|
await test('List events', async () => {
|
|
179
656
|
const res = await api().get('/events');
|
|
@@ -185,15 +662,23 @@ async function runTests() {
|
|
|
185
662
|
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
186
663
|
});
|
|
187
664
|
|
|
188
|
-
await test('Create event', async () => {
|
|
189
|
-
const
|
|
665
|
+
await test('Create event with creator and brand sponsor', async () => {
|
|
666
|
+
const eventData: any = {
|
|
190
667
|
title: 'E2E Test Event',
|
|
191
668
|
description: 'A test event created by E2E tests',
|
|
192
|
-
startDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
669
|
+
startDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
193
670
|
totalSpots: 20,
|
|
194
671
|
locationName: 'Test Location',
|
|
195
672
|
locationAddress: '123 Test Street',
|
|
196
|
-
|
|
673
|
+
gatheringType: 'FACILITATOR_LED',
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
// Add creator and brand if available
|
|
677
|
+
if (state.creatorId) eventData.creatorId = state.creatorId;
|
|
678
|
+
if (state.brandId) eventData.sponsorBrandId = state.brandId;
|
|
679
|
+
if (state.venueId) eventData.venueId = state.venueId;
|
|
680
|
+
|
|
681
|
+
const res = await api(state.accessToken).post('/events', eventData);
|
|
197
682
|
if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
198
683
|
const data = unwrap<any>(res);
|
|
199
684
|
state.eventId = data.id;
|
|
@@ -203,11 +688,18 @@ async function runTests() {
|
|
|
203
688
|
await test('Get event by ID', async () => {
|
|
204
689
|
const res = await api().get(`/events/${state.eventId}`);
|
|
205
690
|
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
691
|
+
const data = unwrap<any>(res);
|
|
692
|
+
// Verify new fields are present
|
|
693
|
+
if (!('gatheringType' in data)) throw new Error('Missing gatheringType field');
|
|
694
|
+
if (!('venueId' in data)) throw new Error('Missing venueId field');
|
|
695
|
+
if (!('creatorId' in data)) throw new Error('Missing creatorId field');
|
|
696
|
+
if (!('sponsorBrandId' in data)) throw new Error('Missing sponsorBrandId field');
|
|
206
697
|
});
|
|
207
698
|
|
|
208
699
|
await test('Update event', async () => {
|
|
209
700
|
const res = await api(state.accessToken).put(`/events/${state.eventId}`, {
|
|
210
701
|
title: 'E2E Test Event (Updated)',
|
|
702
|
+
description: 'Updated description for E2E testing',
|
|
211
703
|
});
|
|
212
704
|
if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
213
705
|
});
|
|
@@ -226,13 +718,12 @@ async function runTests() {
|
|
|
226
718
|
// =========================================================================
|
|
227
719
|
// BOOKINGS
|
|
228
720
|
// =========================================================================
|
|
229
|
-
|
|
721
|
+
section('BOOKINGS');
|
|
230
722
|
|
|
231
723
|
if (state.eventId) {
|
|
232
724
|
await test('RSVP to event', async () => {
|
|
233
725
|
const res = await api(state.accessToken).post(`/events/${state.eventId}/book`);
|
|
234
|
-
|
|
235
|
-
if (res.status !== 201 && res.status !== 409) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
726
|
+
if (res.status !== 201 && res.status !== 409) throw new Error(`Status ${res.status}`);
|
|
236
727
|
if (res.status === 201) {
|
|
237
728
|
const data = unwrap<any>(res);
|
|
238
729
|
state.bookingId = data.id;
|
|
@@ -246,9 +737,9 @@ async function runTests() {
|
|
|
246
737
|
}
|
|
247
738
|
|
|
248
739
|
// =========================================================================
|
|
249
|
-
// EVENT CHAT
|
|
740
|
+
// EVENT CHAT (New roles: ATTENDEE, CREATOR, VENUE_REP, BRAND_REP)
|
|
250
741
|
// =========================================================================
|
|
251
|
-
|
|
742
|
+
section('EVENT CHAT');
|
|
252
743
|
|
|
253
744
|
if (state.eventId) {
|
|
254
745
|
await test('Get or create event chat', async () => {
|
|
@@ -260,25 +751,33 @@ async function runTests() {
|
|
|
260
751
|
|
|
261
752
|
await test('Join event chat', async () => {
|
|
262
753
|
const res = await api(state.accessToken).post(`/events/${state.eventId}/chat/join`);
|
|
263
|
-
|
|
264
|
-
if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
754
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
265
755
|
});
|
|
266
756
|
|
|
267
757
|
await test('Send chat message', async () => {
|
|
268
758
|
const res = await api(state.accessToken).post(`/events/${state.eventId}/chat/messages`, {
|
|
269
759
|
content: 'Hello from E2E test!',
|
|
270
760
|
});
|
|
271
|
-
if (res.status !== 201) throw new Error(`Status ${res.status}
|
|
761
|
+
if (res.status !== 201) throw new Error(`Status ${res.status}`);
|
|
272
762
|
});
|
|
273
763
|
|
|
274
764
|
await test('Get chat messages', async () => {
|
|
275
765
|
const res = await api(state.accessToken).get(`/events/${state.eventId}/chat/messages`);
|
|
276
|
-
if (res.status !== 200) throw new Error(`Status ${res.status}
|
|
766
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
277
767
|
});
|
|
278
768
|
|
|
279
|
-
await test('Get chat members', async () => {
|
|
769
|
+
await test('Get chat members (check for new roles)', async () => {
|
|
280
770
|
const res = await api(state.accessToken).get(`/events/${state.eventId}/chat/members`);
|
|
281
771
|
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
772
|
+
const data = unwrap<any>(res);
|
|
773
|
+
// Verify members have role field
|
|
774
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
775
|
+
const validRoles = ['ATTENDEE', 'CREATOR', 'VENUE_REP', 'BRAND_REP', 'MODERATOR', 'ADMIN'];
|
|
776
|
+
const member = data[0];
|
|
777
|
+
if (!validRoles.includes(member.role)) {
|
|
778
|
+
throw new Error(`Invalid role: ${member.role}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
282
781
|
});
|
|
283
782
|
|
|
284
783
|
await test('Mark chat as read', async () => {
|
|
@@ -287,16 +786,74 @@ async function runTests() {
|
|
|
287
786
|
});
|
|
288
787
|
}
|
|
289
788
|
|
|
789
|
+
// =========================================================================
|
|
790
|
+
// BADGES & UNIFIED REWARDS
|
|
791
|
+
// =========================================================================
|
|
792
|
+
section('BADGES & UNIFIED REWARDS');
|
|
793
|
+
|
|
794
|
+
await test('Get all badges', async () => {
|
|
795
|
+
const res = await api(state.accessToken).get('/badges');
|
|
796
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
797
|
+
const data = unwrap<any>(res);
|
|
798
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
799
|
+
state.badgeId = data[0].id;
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
await test('Get my badges', async () => {
|
|
804
|
+
const res = await api(state.accessToken).get('/badges/users/me/badges');
|
|
805
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
await test('Get next badges (with progress)', async () => {
|
|
809
|
+
const res = await api(state.accessToken).get('/badges/next');
|
|
810
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
await test('Get available rewards', async () => {
|
|
814
|
+
const res = await api(state.accessToken).get('/rewards/available');
|
|
815
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
await test('Get rewards wallet', async () => {
|
|
819
|
+
const res = await api(state.accessToken).get('/rewards/wallet');
|
|
820
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
821
|
+
const data = unwrap<any>(res);
|
|
822
|
+
// Verify wallet structure - badges may be empty array for new users
|
|
823
|
+
if (data.badges === undefined) throw new Error('Missing badges in wallet');
|
|
824
|
+
if (data.stats === undefined) throw new Error('Missing stats in wallet');
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
if (state.badgeId) {
|
|
828
|
+
await test('Get rewards for badge', async () => {
|
|
829
|
+
const res = await api(state.accessToken).get(`/rewards/badge/${state.badgeId}`);
|
|
830
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (state.venueId) {
|
|
835
|
+
await test('Get rewards for venue', async () => {
|
|
836
|
+
const res = await api(state.accessToken).get(`/rewards/venue/${state.venueId}`);
|
|
837
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (state.brandId) {
|
|
842
|
+
await test('Get rewards for brand', async () => {
|
|
843
|
+
const res = await api(state.accessToken).get(`/rewards/brand/${state.brandId}`);
|
|
844
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
290
848
|
// =========================================================================
|
|
291
849
|
// HUBS
|
|
292
850
|
// =========================================================================
|
|
293
|
-
|
|
851
|
+
section('HUBS');
|
|
294
852
|
|
|
295
853
|
await test('List hubs', async () => {
|
|
296
854
|
const res = await api().get('/hubs');
|
|
297
855
|
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
298
|
-
|
|
299
|
-
const data = res.data.data;
|
|
856
|
+
const data = unwrap<any>(res);
|
|
300
857
|
if (Array.isArray(data) && data.length > 0) {
|
|
301
858
|
state.hubId = data[0].id;
|
|
302
859
|
}
|
|
@@ -310,9 +867,8 @@ async function runTests() {
|
|
|
310
867
|
|
|
311
868
|
await test('Join hub', async () => {
|
|
312
869
|
const res = await api(state.accessToken).post(`/hubs/${state.hubId}/join`);
|
|
313
|
-
// 200/201 for join, 409 if already member
|
|
314
870
|
if (res.status !== 200 && res.status !== 201 && res.status !== 409) {
|
|
315
|
-
throw new Error(`Status ${res.status}
|
|
871
|
+
throw new Error(`Status ${res.status}`);
|
|
316
872
|
}
|
|
317
873
|
});
|
|
318
874
|
|
|
@@ -322,16 +878,37 @@ async function runTests() {
|
|
|
322
878
|
});
|
|
323
879
|
}
|
|
324
880
|
|
|
881
|
+
// =========================================================================
|
|
882
|
+
// LIBRARY (Content workflow)
|
|
883
|
+
// =========================================================================
|
|
884
|
+
section('LIBRARY');
|
|
885
|
+
|
|
886
|
+
await test('Get library content', async () => {
|
|
887
|
+
const res = await api(state.accessToken).get('/library');
|
|
888
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
await test('Get featured content', async () => {
|
|
892
|
+
const res = await api(state.accessToken).get('/library/featured');
|
|
893
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
// Content workflow tests (if creator exists)
|
|
897
|
+
if (state.creatorId) {
|
|
898
|
+
// Note: /library/creator/:id endpoint not implemented yet
|
|
899
|
+
// skip('Get content by creator', 'Endpoint not implemented');
|
|
900
|
+
}
|
|
901
|
+
|
|
325
902
|
// =========================================================================
|
|
326
903
|
// JACK AI (Support Conversations)
|
|
327
904
|
// =========================================================================
|
|
328
|
-
|
|
905
|
+
section('JACK AI');
|
|
329
906
|
|
|
330
907
|
await test('Start Jack conversation', async () => {
|
|
331
908
|
const res = await api(state.accessToken).post('/support/jack/conversations/new', {
|
|
332
909
|
message: 'Hello Jack!',
|
|
333
910
|
});
|
|
334
|
-
if (res.status !== 201 && res.status !== 200) throw new Error(`Status ${res.status}
|
|
911
|
+
if (res.status !== 201 && res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
335
912
|
const data = unwrap<any>(res);
|
|
336
913
|
state.conversationId = data.conversation?.id || data.id;
|
|
337
914
|
});
|
|
@@ -348,7 +925,9 @@ async function runTests() {
|
|
|
348
925
|
|
|
349
926
|
if (state.conversationId) {
|
|
350
927
|
await test('Get Jack conversation by ID', async () => {
|
|
351
|
-
const res = await api(state.accessToken).get(
|
|
928
|
+
const res = await api(state.accessToken).get(
|
|
929
|
+
`/support/jack/conversations/${state.conversationId}`
|
|
930
|
+
);
|
|
352
931
|
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
353
932
|
});
|
|
354
933
|
}
|
|
@@ -356,18 +935,17 @@ async function runTests() {
|
|
|
356
935
|
// =========================================================================
|
|
357
936
|
// SUPPORT (Check-ins, Wins)
|
|
358
937
|
// =========================================================================
|
|
359
|
-
|
|
938
|
+
section('SUPPORT');
|
|
360
939
|
|
|
361
940
|
await test('Create daily check-in', async () => {
|
|
362
941
|
const res = await api(state.accessToken).post('/support/check-ins', {
|
|
363
942
|
date: new Date().toISOString().split('T')[0],
|
|
364
|
-
mood: 4,
|
|
365
|
-
energy: 4,
|
|
943
|
+
mood: 4,
|
|
944
|
+
energy: 4,
|
|
366
945
|
stayedSober: true,
|
|
367
946
|
notes: 'E2E test check-in',
|
|
368
947
|
});
|
|
369
|
-
|
|
370
|
-
if (res.status !== 201 && res.status !== 409) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
948
|
+
if (res.status !== 201 && res.status !== 409) throw new Error(`Status ${res.status}`);
|
|
371
949
|
});
|
|
372
950
|
|
|
373
951
|
await test('Get check-ins', async () => {
|
|
@@ -389,9 +967,9 @@ async function runTests() {
|
|
|
389
967
|
const res = await api(state.accessToken).post('/support/wins', {
|
|
390
968
|
title: 'Completed E2E tests',
|
|
391
969
|
description: 'Successfully ran all API tests',
|
|
392
|
-
category: 'PERSONAL',
|
|
970
|
+
category: 'PERSONAL',
|
|
393
971
|
});
|
|
394
|
-
if (res.status !== 201) throw new Error(`Status ${res.status}
|
|
972
|
+
if (res.status !== 201) throw new Error(`Status ${res.status}`);
|
|
395
973
|
});
|
|
396
974
|
|
|
397
975
|
await test('Get wins', async () => {
|
|
@@ -407,7 +985,7 @@ async function runTests() {
|
|
|
407
985
|
// =========================================================================
|
|
408
986
|
// NOTIFICATIONS
|
|
409
987
|
// =========================================================================
|
|
410
|
-
|
|
988
|
+
section('NOTIFICATIONS');
|
|
411
989
|
|
|
412
990
|
await test('Get notifications', async () => {
|
|
413
991
|
const res = await api(state.accessToken).get('/notifications');
|
|
@@ -422,7 +1000,7 @@ async function runTests() {
|
|
|
422
1000
|
// =========================================================================
|
|
423
1001
|
// SUBSCRIPTIONS
|
|
424
1002
|
// =========================================================================
|
|
425
|
-
|
|
1003
|
+
section('SUBSCRIPTIONS');
|
|
426
1004
|
|
|
427
1005
|
await test('Get subscription plans', async () => {
|
|
428
1006
|
const res = await api(state.accessToken).get('/subscriptions/plans');
|
|
@@ -431,44 +1009,384 @@ async function runTests() {
|
|
|
431
1009
|
|
|
432
1010
|
await test('Get my subscription', async () => {
|
|
433
1011
|
const res = await api(state.accessToken).get('/subscriptions/me');
|
|
434
|
-
// 200 if subscribed, 404 if not
|
|
435
1012
|
if (res.status !== 200 && res.status !== 404) throw new Error(`Status ${res.status}`);
|
|
436
1013
|
});
|
|
437
1014
|
|
|
438
1015
|
// =========================================================================
|
|
439
|
-
//
|
|
1016
|
+
// MAP
|
|
440
1017
|
// =========================================================================
|
|
441
|
-
|
|
1018
|
+
section('MAP');
|
|
442
1019
|
|
|
443
|
-
await test('Get
|
|
444
|
-
const res = await api(state.accessToken).get('/
|
|
1020
|
+
await test('Get map hubs', async () => {
|
|
1021
|
+
const res = await api(state.accessToken).get('/map/hubs');
|
|
445
1022
|
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
446
1023
|
});
|
|
447
1024
|
|
|
448
|
-
await test('Get
|
|
449
|
-
const res = await api(state.accessToken).get('/
|
|
1025
|
+
await test('Get map events', async () => {
|
|
1026
|
+
const res = await api(state.accessToken).get('/map/events');
|
|
450
1027
|
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
451
1028
|
});
|
|
452
1029
|
|
|
453
1030
|
// =========================================================================
|
|
454
|
-
//
|
|
1031
|
+
// BADGE AWARDING (Business Logic Tests)
|
|
455
1032
|
// =========================================================================
|
|
456
|
-
|
|
1033
|
+
section('BADGE AWARDING');
|
|
457
1034
|
|
|
458
|
-
|
|
459
|
-
|
|
1035
|
+
// Test badge progress tracking after actions
|
|
1036
|
+
// Note: These tests verify the badge awarding integration works correctly.
|
|
1037
|
+
// Actual badge earning depends on thresholds in seed data.
|
|
1038
|
+
|
|
1039
|
+
// Get initial badge state
|
|
1040
|
+
let initialBadgeCount = 0;
|
|
1041
|
+
let initialBadges: string[] = [];
|
|
1042
|
+
|
|
1043
|
+
await test('Get initial user badges before badge tests', async () => {
|
|
1044
|
+
const res = await api(state.accessToken).get('/badges/users/me/badges');
|
|
460
1045
|
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
1046
|
+
const data = unwrap<any>(res);
|
|
1047
|
+
initialBadgeCount = Array.isArray(data) ? data.length : 0;
|
|
1048
|
+
initialBadges = Array.isArray(data) ? data.map((b: any) => b.badgeId) : [];
|
|
461
1049
|
});
|
|
462
1050
|
|
|
463
|
-
|
|
464
|
-
|
|
1051
|
+
// Check-in Streak Badge Tests
|
|
1052
|
+
await test('Check-in updates streak and triggers badge check', async () => {
|
|
1053
|
+
// Create a check-in for a previous day (to test streak logic)
|
|
1054
|
+
const yesterday = new Date();
|
|
1055
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
1056
|
+
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
|
1057
|
+
|
|
1058
|
+
const res = await api(state.accessToken).post('/support/check-ins', {
|
|
1059
|
+
date: yesterdayStr,
|
|
1060
|
+
mood: 4,
|
|
1061
|
+
energy: 3,
|
|
1062
|
+
stayedSober: true,
|
|
1063
|
+
notes: 'Badge test check-in',
|
|
1064
|
+
});
|
|
1065
|
+
// Accept 201 (created) or 409 (already exists - re-running test)
|
|
1066
|
+
if (res.status !== 201 && res.status !== 409) throw new Error(`Status ${res.status}`);
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
await test('Verify streak is tracked after check-in', async () => {
|
|
1070
|
+
const res = await api(state.accessToken).get('/support/check-ins/streak');
|
|
1071
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
1072
|
+
const data = unwrap<any>(res);
|
|
1073
|
+
// Should have at least 1 check-in now (from earlier test)
|
|
1074
|
+
if (data.totalCheckIns === undefined) throw new Error('Missing totalCheckIns in streak data');
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
// Wins Badge Tests
|
|
1078
|
+
await test('Log win updates count and triggers badge check', async () => {
|
|
1079
|
+
const res = await api(state.accessToken).post('/support/wins', {
|
|
1080
|
+
title: 'Badge awarding test win',
|
|
1081
|
+
description: 'Testing that logging wins triggers badge check',
|
|
1082
|
+
category: 'PERSONAL', // Valid WinCategory enum value
|
|
1083
|
+
});
|
|
1084
|
+
if (res.status !== 201) throw new Error(`Status ${res.status}`);
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
await test('Verify wins count is tracked', async () => {
|
|
1088
|
+
const res = await api(state.accessToken).get('/support/wins/count');
|
|
465
1089
|
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
1090
|
+
const data = unwrap<any>(res);
|
|
1091
|
+
// Should have totalWins or count
|
|
1092
|
+
if (data.totalWins === undefined && typeof data !== 'number' && data.count === undefined) {
|
|
1093
|
+
throw new Error('Expected wins count in response');
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
// Mood Log Badge Tests
|
|
1098
|
+
await test('Log mood updates count and triggers badge check', async () => {
|
|
1099
|
+
const res = await api(state.accessToken).post('/support/mood-logs', {
|
|
1100
|
+
mood: 4, // Required field (1-5)
|
|
1101
|
+
energy: 3, // Optional (1-5)
|
|
1102
|
+
tags: ['hopeful'], // Optional array of strings
|
|
1103
|
+
note: 'Badge test mood log', // Optional note
|
|
1104
|
+
});
|
|
1105
|
+
if (res.status !== 201) throw new Error(`Status ${res.status}`);
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
await test('Verify mood logs are tracked', async () => {
|
|
1109
|
+
const res = await api(state.accessToken).get('/support/mood-logs');
|
|
1110
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
1111
|
+
const data = unwrap<any>(res);
|
|
1112
|
+
// Handle both array and { data: array } formats
|
|
1113
|
+
const logs = Array.isArray(data) ? data : (data.data || data.moodLogs || []);
|
|
1114
|
+
// Should have at least 1 mood log
|
|
1115
|
+
if (!Array.isArray(logs) || logs.length === 0) {
|
|
1116
|
+
throw new Error('Expected at least one mood log');
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
// Reflection Badge Tests
|
|
1121
|
+
await test('Submit reflection updates count and triggers badge check', async () => {
|
|
1122
|
+
// Calculate weekStart - needs to be a Monday in YYYY-MM-DD format
|
|
1123
|
+
const now = new Date();
|
|
1124
|
+
const dayOfWeek = now.getDay();
|
|
1125
|
+
const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
|
1126
|
+
const monday = new Date(now);
|
|
1127
|
+
monday.setDate(now.getDate() - daysToMonday);
|
|
1128
|
+
const weekStart = monday.toISOString().split('T')[0];
|
|
1129
|
+
|
|
1130
|
+
const res = await api(state.accessToken).post('/support/reflections', {
|
|
1131
|
+
weekStart, // Required field
|
|
1132
|
+
overallRating: 4,
|
|
1133
|
+
biggestWin: 'Badge test reflection - testing the badge integration',
|
|
1134
|
+
challenges: 'Testing automated badge checks',
|
|
1135
|
+
goals: ['Continue improving the platform'],
|
|
1136
|
+
notes: 'E2E test reflection',
|
|
1137
|
+
});
|
|
1138
|
+
// Accept 201 (created) or 409 (already exists for this week)
|
|
1139
|
+
if (res.status !== 201 && res.status !== 409) throw new Error(`Status ${res.status}`);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
await test('Verify reflections are tracked', async () => {
|
|
1143
|
+
const res = await api(state.accessToken).get('/support/reflections');
|
|
1144
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
// Events Hosted Badge Tests (awarded on publish)
|
|
1148
|
+
await test('Publishing event awards EVENTS_HOSTED badge to hosts', async () => {
|
|
1149
|
+
// The event was already published in EVENTS section
|
|
1150
|
+
// Verify the badge check was triggered by checking next badges
|
|
1151
|
+
const res = await api(state.accessToken).get('/badges/next');
|
|
1152
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
1153
|
+
const data = unwrap<any>(res);
|
|
1154
|
+
// Should return badge progress data
|
|
1155
|
+
if (!Array.isArray(data)) throw new Error('Expected array of next badges');
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
// Events Attended Badge Tests
|
|
1159
|
+
// Note: EVENTS_ATTENDED badge is awarded when booking is checked-in
|
|
1160
|
+
// This requires a host to check-in the booking, which we can't do in E2E easily
|
|
1161
|
+
// So we just verify the badge progress tracking works
|
|
1162
|
+
|
|
1163
|
+
await test('Badge progress shows EVENTS_ATTENDED tracking', async () => {
|
|
1164
|
+
const res = await api(state.accessToken).get('/badges/next');
|
|
1165
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
1166
|
+
const data = unwrap<any>(res);
|
|
1167
|
+
// Look for events attended badge in progress
|
|
1168
|
+
const eventsAttendedBadge = data.find(
|
|
1169
|
+
(b: any) => b.badge?.type === 'EVENTS_ATTENDED' || b.badge?.slug?.includes('event')
|
|
1170
|
+
);
|
|
1171
|
+
// May or may not exist depending on seed data, but API should work
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
// Verify badge state after all actions
|
|
1175
|
+
await test('Verify badges after all badge-triggering actions', async () => {
|
|
1176
|
+
const res = await api(state.accessToken).get('/badges/users/me/badges');
|
|
1177
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
1178
|
+
const data = unwrap<any>(res);
|
|
1179
|
+
const currentBadgeCount = Array.isArray(data) ? data.length : 0;
|
|
1180
|
+
// Depending on thresholds, user may have earned new badges
|
|
1181
|
+
// At minimum, the API should return successfully
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
// =========================================================================
|
|
1185
|
+
// REWARD FLOW
|
|
1186
|
+
// =========================================================================
|
|
1187
|
+
section('REWARD FLOW');
|
|
1188
|
+
|
|
1189
|
+
// Store reward-related state
|
|
1190
|
+
let testBadgeIdForReward: string | undefined;
|
|
1191
|
+
let testRewardId: string | undefined;
|
|
1192
|
+
let redemptionId: string | undefined;
|
|
1193
|
+
|
|
1194
|
+
// Get a badge to use for reward tests
|
|
1195
|
+
await test('Get available badges for reward test', async () => {
|
|
1196
|
+
const res = await api(state.accessToken).get('/badges');
|
|
1197
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
1198
|
+
const data = unwrap<any>(res);
|
|
1199
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
1200
|
+
testBadgeIdForReward = data[0].id;
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
// Create a brand reward for testing (user owns brand from earlier tests)
|
|
1205
|
+
// Note: We use brand since we created one in the BRANDS section
|
|
1206
|
+
if (state.brandId) {
|
|
1207
|
+
await test('Create brand reward for badge', async () => {
|
|
1208
|
+
if (!testBadgeIdForReward) {
|
|
1209
|
+
throw new Error('No badge available to create reward for');
|
|
1210
|
+
}
|
|
1211
|
+
const res = await api(state.accessToken).post(`/brands/${state.brandId}/rewards`, {
|
|
1212
|
+
badgeId: testBadgeIdForReward,
|
|
1213
|
+
title: 'E2E Brand Test Reward',
|
|
1214
|
+
description: 'A test brand reward for E2E testing',
|
|
1215
|
+
redeemType: 'ONLINE',
|
|
1216
|
+
code: 'E2E-TEST-CODE',
|
|
1217
|
+
perUserLimit: 1,
|
|
1218
|
+
});
|
|
1219
|
+
if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
1220
|
+
const data = unwrap<any>(res);
|
|
1221
|
+
testRewardId = data.id;
|
|
1222
|
+
});
|
|
1223
|
+
} else {
|
|
1224
|
+
skip('Create brand reward for badge', 'No brand available (brand not created)');
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Test reward availability
|
|
1228
|
+
await test('Get available rewards for user', async () => {
|
|
1229
|
+
const res = await api(state.accessToken).get('/rewards/available');
|
|
1230
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
1231
|
+
const data = unwrap<any>(res);
|
|
1232
|
+
// API returns { data: rewards[] }
|
|
1233
|
+
const rewards = data.data || data;
|
|
1234
|
+
if (!Array.isArray(rewards)) throw new Error('Expected array of available rewards');
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
// Test user cannot redeem without earning the badge
|
|
1238
|
+
await test('Cannot redeem reward without earning required badge (400)', async () => {
|
|
1239
|
+
if (!testRewardId || !testBadgeIdForReward) {
|
|
1240
|
+
console.log(` ${c.dim}(Skipped: No test reward created)${c.reset}`);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// First check if user has the badge
|
|
1245
|
+
const badgeRes = await api(state.accessToken).get('/badges/users/me/badges');
|
|
1246
|
+
const badges = unwrap<any>(badgeRes);
|
|
1247
|
+
const hasBadge = Array.isArray(badges) && badges.some(
|
|
1248
|
+
(b: any) => b.badgeId === testBadgeIdForReward
|
|
1249
|
+
);
|
|
1250
|
+
|
|
1251
|
+
if (!hasBadge) {
|
|
1252
|
+
// User doesn't have badge, should get 400 when trying to redeem
|
|
1253
|
+
const res = await api(state.accessToken).post(`/rewards/${testRewardId}/redeem`);
|
|
1254
|
+
if (res.status !== 400) {
|
|
1255
|
+
throw new Error(`Expected 400 (need badge first), got ${res.status}`);
|
|
1256
|
+
}
|
|
1257
|
+
} else {
|
|
1258
|
+
// User has the badge, test passes (we'll test redemption below)
|
|
1259
|
+
console.log(` ${c.dim}(User already has required badge - will test redemption)${c.reset}`);
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
// Test redemption flow - check if user can redeem
|
|
1264
|
+
await test('Check badge ownership and test redemption flow', async () => {
|
|
1265
|
+
if (!testRewardId || !testBadgeIdForReward) {
|
|
1266
|
+
console.log(` ${c.dim}(Skipped: No test reward created)${c.reset}`);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Check if user has earned the badge
|
|
1271
|
+
const badgeRes = await api(state.accessToken).get('/badges/users/me/badges');
|
|
1272
|
+
if (badgeRes.status !== 200) throw new Error(`Status ${badgeRes.status}`);
|
|
1273
|
+
const badges = unwrap<any>(badgeRes);
|
|
1274
|
+
const userHasBadge = Array.isArray(badges) && badges.some(
|
|
1275
|
+
(b: any) => b.badgeId === testBadgeIdForReward
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1278
|
+
if (userHasBadge) {
|
|
1279
|
+
// User has badge, test the full redemption flow
|
|
1280
|
+
const redeemRes = await api(state.accessToken).post(`/rewards/${testRewardId}/redeem`);
|
|
1281
|
+
if (redeemRes.status !== 201) {
|
|
1282
|
+
throw new Error(`Redeem failed: ${redeemRes.status}: ${JSON.stringify(redeemRes.data)}`);
|
|
1283
|
+
}
|
|
1284
|
+
const redeemData = unwrap<any>(redeemRes);
|
|
1285
|
+
redemptionId = redeemData.id;
|
|
1286
|
+
console.log(` ${c.dim}(Redemption successful: ${redemptionId})${c.reset}`);
|
|
1287
|
+
} else {
|
|
1288
|
+
console.log(` ${c.dim}(User has not earned required badge - redemption test skipped)${c.reset}`);
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
await test('Cannot redeem same reward twice (400)', async () => {
|
|
1293
|
+
if (!testRewardId || !redemptionId) {
|
|
1294
|
+
console.log(` ${c.dim}(Skipped: No prior redemption to test)${c.reset}`);
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
const res = await api(state.accessToken).post(`/rewards/${testRewardId}/redeem`);
|
|
1298
|
+
if (res.status !== 400) {
|
|
1299
|
+
throw new Error(`Expected 400 (already redeemed), got ${res.status}`);
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
await test('Get redeemed rewards', async () => {
|
|
1304
|
+
const res = await api(state.accessToken).get('/rewards/redeemed');
|
|
1305
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
1306
|
+
const data = unwrap<any>(res);
|
|
1307
|
+
// API returns { data: redemptions[] }
|
|
1308
|
+
const redemptions = data.data || data;
|
|
1309
|
+
if (!Array.isArray(redemptions)) throw new Error('Expected array of redeemed rewards');
|
|
1310
|
+
// If we redeemed, should have at least one
|
|
1311
|
+
if (redemptionId && redemptions.length === 0) {
|
|
1312
|
+
throw new Error('Expected at least one redeemed reward');
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
await test('Rewards wallet shows correct stats', async () => {
|
|
1317
|
+
const res = await api(state.accessToken).get('/rewards/wallet');
|
|
1318
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
1319
|
+
const data = unwrap<any>(res);
|
|
1320
|
+
if (data.stats === undefined) throw new Error('Missing stats in wallet');
|
|
1321
|
+
// After redemption, should have at least 1 redeemed
|
|
1322
|
+
if (redemptionId && data.stats.totalRedeemedRewards < 1) {
|
|
1323
|
+
throw new Error('Expected at least 1 redeemed reward in wallet stats');
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
// Test getting rewards by badge
|
|
1328
|
+
await test('Get rewards for specific badge', async () => {
|
|
1329
|
+
if (!testBadgeIdForReward) {
|
|
1330
|
+
console.log(` ${c.dim}(Skipped: No badge ID available)${c.reset}`);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
const res = await api(state.accessToken).get(`/rewards/badge/${testBadgeIdForReward}`);
|
|
1334
|
+
if (res.status !== 200) throw new Error(`Status ${res.status}`);
|
|
1335
|
+
const data = unwrap<any>(res);
|
|
1336
|
+
// API returns { data: rewards[] }
|
|
1337
|
+
const rewards = data.data || data;
|
|
1338
|
+
if (!Array.isArray(rewards)) throw new Error('Expected array of rewards');
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
// Verify reward redemption verification endpoint
|
|
1342
|
+
await test('Verify redemption endpoint works', async () => {
|
|
1343
|
+
// This would normally be called by venue/brand staff
|
|
1344
|
+
// We'll test with an invalid token to ensure endpoint exists
|
|
1345
|
+
const res = await api(state.accessToken).post('/rewards/verify', {
|
|
1346
|
+
qrToken: 'invalid-test-token',
|
|
1347
|
+
});
|
|
1348
|
+
// POST defaults to 201, but either 200 or 201 is acceptable
|
|
1349
|
+
if (res.status !== 200 && res.status !== 201) throw new Error(`Status ${res.status}`);
|
|
1350
|
+
const data = unwrap<any>(res);
|
|
1351
|
+
// API returns { data: { valid: boolean } }
|
|
1352
|
+
const result = data.data || data;
|
|
1353
|
+
if (result.valid !== false) {
|
|
1354
|
+
throw new Error('Expected valid: false for invalid token');
|
|
1355
|
+
}
|
|
466
1356
|
});
|
|
467
1357
|
|
|
468
1358
|
// =========================================================================
|
|
469
1359
|
// CLEANUP
|
|
470
1360
|
// =========================================================================
|
|
471
|
-
|
|
1361
|
+
section('CLEANUP');
|
|
1362
|
+
|
|
1363
|
+
// Cleanup creator products
|
|
1364
|
+
if (state.productId && state.creatorId) {
|
|
1365
|
+
await test('Delete creator product', async () => {
|
|
1366
|
+
const res = await api(state.accessToken).delete(
|
|
1367
|
+
`/creators/${state.creatorId}/products/${state.productId}`
|
|
1368
|
+
);
|
|
1369
|
+
if (res.status !== 200 && res.status !== 204) throw new Error(`Status ${res.status}`);
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
if (state.availabilityId && state.creatorId) {
|
|
1374
|
+
await test('Remove creator availability', async () => {
|
|
1375
|
+
const res = await api(state.accessToken).delete(
|
|
1376
|
+
`/creators/${state.creatorId}/availability/${state.availabilityId}`
|
|
1377
|
+
);
|
|
1378
|
+
if (res.status !== 200 && res.status !== 204) throw new Error(`Status ${res.status}`);
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (state.creatorId && state.brandId) {
|
|
1383
|
+
await test('Remove creator from brand partnership', async () => {
|
|
1384
|
+
const res = await api(state.accessToken).delete(
|
|
1385
|
+
`/brands/${state.brandId}/creators/${state.creatorId}`
|
|
1386
|
+
);
|
|
1387
|
+
if (res.status !== 200 && res.status !== 204) throw new Error(`Status ${res.status}`);
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
472
1390
|
|
|
473
1391
|
if (state.bookingId) {
|
|
474
1392
|
await test('Cancel booking', async () => {
|
|
@@ -487,16 +1405,30 @@ async function runTests() {
|
|
|
487
1405
|
// =========================================================================
|
|
488
1406
|
// SUMMARY
|
|
489
1407
|
// =========================================================================
|
|
490
|
-
console.log(`\n${c.cyan}${'='.repeat(
|
|
1408
|
+
console.log(`\n${c.cyan}${'='.repeat(70)}${c.reset}`);
|
|
491
1409
|
console.log(`${c.cyan} TEST SUMMARY${c.reset}`);
|
|
492
|
-
console.log(`${c.cyan}${'='.repeat(
|
|
493
|
-
console.log(`${c.green}Passed:
|
|
494
|
-
console.log(`${c.red}Failed:
|
|
495
|
-
console.log(
|
|
496
|
-
console.log(`${c.
|
|
1410
|
+
console.log(`${c.cyan}${'='.repeat(70)}${c.reset}`);
|
|
1411
|
+
console.log(`${c.green}Passed: ${passed}${c.reset}`);
|
|
1412
|
+
console.log(`${c.red}Failed: ${failed}${c.reset}`);
|
|
1413
|
+
console.log(`${c.yellow}Skipped: ${skipped}${c.reset}`);
|
|
1414
|
+
console.log(`${c.bold}Total: ${passed + failed + skipped}${c.reset}`);
|
|
1415
|
+
console.log(`${c.cyan}${'='.repeat(70)}${c.reset}\n`);
|
|
1416
|
+
|
|
1417
|
+
// IDs for debugging
|
|
1418
|
+
console.log(`${c.dim}Test IDs:${c.reset}`);
|
|
1419
|
+
console.log(`${c.dim} User: ${state.userId}${c.reset}`);
|
|
1420
|
+
console.log(`${c.dim} Creator: ${state.creatorId || 'N/A'}${c.reset}`);
|
|
1421
|
+
console.log(`${c.dim} Product: ${state.productId || 'N/A'}${c.reset}`);
|
|
1422
|
+
console.log(`${c.dim} Brand: ${state.brandId || 'N/A'}${c.reset}`);
|
|
1423
|
+
console.log(`${c.dim} Event: ${state.eventId || 'N/A'}${c.reset}`);
|
|
1424
|
+
console.log(`${c.dim} Venue: ${state.venueId || 'N/A'}${c.reset}`);
|
|
1425
|
+
console.log('');
|
|
497
1426
|
|
|
498
1427
|
process.exit(failed > 0 ? 1 : 0);
|
|
499
1428
|
}
|
|
500
1429
|
|
|
501
1430
|
// Run tests
|
|
502
|
-
runTests().catch(
|
|
1431
|
+
runTests().catch((e) => {
|
|
1432
|
+
console.error(`${c.red}Unexpected error:${c.reset}`, e);
|
|
1433
|
+
process.exit(1);
|
|
1434
|
+
});
|