@edifice.io/communities-tests 1.0.0-develop-pedago.20250725171105

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.
@@ -0,0 +1,868 @@
1
+ import { check, group } from "k6";
2
+ // @ts-ignore
3
+ import { chai, describe } from "https://jslib.k6.io/k6chaijs/4.3.4.0/index.js";
4
+ import { sleep } from "k6";
5
+
6
+ import {
7
+ authenticateWeb,
8
+ getUsersOfSchool,
9
+ getRandomUserWithProfile,
10
+ initStructure,
11
+ Structure,
12
+ logout,
13
+ createAndSetRole,
14
+ getRolesOfStructure,
15
+ linkRoleToUsers,
16
+ UserInfo,
17
+ } from "../../node_modules/edifice-k6-commons/dist/index.js";
18
+
19
+ import {
20
+ createCommunityOrFail,
21
+ getCommunity,
22
+ getSecretCode,
23
+ getCommunityStats,
24
+ updateCommunity,
25
+ updateWelcomeNote,
26
+ deleteCommunity,
27
+ listCommunities,
28
+ } from "./utils/_community-api.utils.ts";
29
+
30
+ import {
31
+ listCommunityMembers,
32
+ updateMemberRole,
33
+ leaveCommunity,
34
+ removeMember,
35
+ removeMembers,
36
+ } from "./utils/_membership-api.utils.ts";
37
+
38
+ import {
39
+ createInvitations,
40
+ listCommunityInvitations,
41
+ MembershipRole,
42
+ deleteInvitation,
43
+ deleteInvitationsBatch,
44
+ listMyInvitations,
45
+ updateInvitationStatus,
46
+ InvitationStatus,
47
+ joinCommunity,
48
+ updateRequestStatus,
49
+ } from "./utils/_invitation-api.utils.ts";
50
+ import {
51
+ AppName,
52
+ countResources,
53
+ createResource,
54
+ deleteResource,
55
+ getResource,
56
+ listCommunityResources,
57
+ processMarkedResources,
58
+ ResourceType,
59
+ updateResource,
60
+ } from "./utils/_resource-api.utils.ts";
61
+
62
+ // Add chai configuration
63
+ chai.config.logFailures = true;
64
+
65
+ // Add K6 options configuration
66
+ export const options = {
67
+ setupTimeout: "1h",
68
+ maxRedirects: 0,
69
+ thresholds: {
70
+ checks: ["rate == 1.00"],
71
+ },
72
+ scenarios: {
73
+ roleGuardTest: {
74
+ exec: "testCommunityRoleGuards",
75
+ executor: "per-vu-iterations",
76
+ vus: 1,
77
+ maxDuration: "1m30s", // Slightly longer for role tests
78
+ gracefulStop: "5s",
79
+ },
80
+ },
81
+ };
82
+
83
+ const schoolName = `IT Community Roles`;
84
+
85
+ // Setup function to create structure and configure roles
86
+ export function setup(): Structure {
87
+ let structure: Structure = {} as Structure;
88
+ describe("[Community Roles] Initialize data", () => {
89
+ authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD);
90
+ structure = initStructure(schoolName, "tiny");
91
+
92
+ // Create Communities role
93
+ const role = createAndSetRole("Communities");
94
+
95
+ // Assign role to all groups/users in structure
96
+ const groups = getRolesOfStructure(structure.id);
97
+ linkRoleToUsers(
98
+ structure,
99
+ role,
100
+ groups.map((g: any) => g.name),
101
+ );
102
+ });
103
+ return structure;
104
+ }
105
+
106
+ /**
107
+ * Test to verify that RequireCommunityRole guards restrict access
108
+ * based on the user's role in a community
109
+ */
110
+ export function testCommunityRoleGuards(structure: Structure) {
111
+ let adminUser: UserInfo;
112
+ let memberUser: UserInfo;
113
+ let nonMemberUser: UserInfo;
114
+ let communityId: number;
115
+ let communitySecretCode: string;
116
+ let resourceId: number; // Add this to track test resource ID
117
+
118
+ describe("[Community API Role Guards Test]", () => {
119
+ // First authenticate as admin to get users
120
+ authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD);
121
+
122
+ // Use structure from parameter instead of initializing
123
+ const users = getUsersOfSchool(structure);
124
+
125
+ // Get test users while still authenticated as admin
126
+ adminUser = getRandomUserWithProfile(users, "Teacher");
127
+ memberUser = getRandomUserWithProfile(users, "Student", [adminUser]);
128
+ nonMemberUser = getRandomUserWithProfile(users, "Student", [
129
+ adminUser,
130
+ memberUser,
131
+ ]);
132
+
133
+ // Log out after getting users
134
+ logout();
135
+
136
+ // Create a test community with the admin user
137
+ group("Setup: Admin creates a test community", () => {
138
+ authenticateWeb(adminUser.login, "password");
139
+
140
+ communityId = Number(
141
+ createCommunityOrFail({
142
+ title: "Role Test Community",
143
+ type: "CLASS",
144
+ schoolYearStart: 2025,
145
+ schoolYearEnd: 2026,
146
+ }),
147
+ );
148
+ console.log(`Created test community with ID: ${communityId}`);
149
+
150
+ // Get the secret code
151
+ const secretCode = getSecretCode(communityId);
152
+ check(secretCode, {
153
+ "Admin can get secret code": (r) => r !== null,
154
+ });
155
+
156
+ if (secretCode) {
157
+ communitySecretCode = secretCode;
158
+ console.log(`Retrieved secret code: ${communitySecretCode}`);
159
+ }
160
+
161
+ // Add resource creation for testing
162
+ const resource = createResource(communityId, {
163
+ type: ResourceType.ENT,
164
+ appName: AppName.EXTERNAL_LINK,
165
+ title: "Test Resource for Role Checks",
166
+ resourceUrl: "https://example.com/test",
167
+ openInNewTab: true,
168
+ });
169
+
170
+ check(resource, {
171
+ "Admin can create a test resource": (r) => r !== null,
172
+ });
173
+
174
+ if (resource) {
175
+ resourceId = resource.id;
176
+ console.log(`Created test resource with ID: ${resourceId}`);
177
+ }
178
+
179
+ logout();
180
+ });
181
+
182
+ // Add a regular member to the community
183
+ group("Setup: Add a regular member to the community", () => {
184
+ // 1. The member user joins the community
185
+ authenticateWeb(memberUser.login, "password");
186
+
187
+ const joinRequest = joinCommunity(communitySecretCode);
188
+ check(joinRequest, {
189
+ "Join request created successfully": (r) => r !== null,
190
+ "Join request has REQUEST status": (r) =>
191
+ r !== null && r.status === InvitationStatus.REQUEST,
192
+ });
193
+
194
+ // Save the request ID for later use
195
+ const requestInvitationId = joinRequest?.id;
196
+ console.log(`Created join request with ID: ${requestInvitationId}`);
197
+
198
+ logout();
199
+
200
+ // 2. Admin approves the join request
201
+ authenticateWeb(adminUser.login, "password");
202
+
203
+ const approvedRequest = updateRequestStatus(
204
+ communityId,
205
+ requestInvitationId!,
206
+ InvitationStatus.REQUEST_ACCEPTED,
207
+ );
208
+
209
+ check(approvedRequest, {
210
+ "Admin approved join request successfully": (r) => r !== null,
211
+ "Status changed to REQUEST_ACCEPTED": (r) =>
212
+ r !== null && r.status === InvitationStatus.REQUEST_ACCEPTED,
213
+ });
214
+
215
+ // 3. Check if the member is now part of the community
216
+ const members = listCommunityMembers(communityId);
217
+ const isMember = members?.items.some(
218
+ (m) => m.user.entId === memberUser.id,
219
+ );
220
+
221
+ check(isMember, {
222
+ "User is now a community member after admin approval": (result) =>
223
+ result === true,
224
+ });
225
+
226
+ logout();
227
+ });
228
+
229
+ // Test access with non-member user
230
+ group("Test API access with non-member user", () => {
231
+ authenticateWeb(nonMemberUser.login, "password");
232
+
233
+ // Tests requiring MEMBER or ADMIN roles (should all fail)
234
+ const community = getCommunity(communityId);
235
+ check(community, {
236
+ "Non-member can get community details": (c) => c === null,
237
+ });
238
+
239
+ const secretCode = getSecretCode(communityId);
240
+ check(secretCode, {
241
+ "Non-member cannot get secret code": (s) => s === null,
242
+ });
243
+
244
+ const stats = getCommunityStats(String(communityId));
245
+ check(stats, {
246
+ "Non-member cannot get community stats": (s) => s === null,
247
+ });
248
+
249
+ const updatedCommunity = updateCommunity(String(communityId), {
250
+ title: "Updated Title",
251
+ type: "CLASS",
252
+ });
253
+ check(updatedCommunity, {
254
+ "Non-member cannot update community": (c) => c === null,
255
+ });
256
+
257
+ const welcomeNote = updateWelcomeNote(
258
+ String(communityId),
259
+ "New welcome note",
260
+ );
261
+ check(welcomeNote, {
262
+ "Non-member cannot update welcome note": (w) => w === null,
263
+ });
264
+
265
+ const members = listCommunityMembers(communityId);
266
+ check(members, {
267
+ "Non-member cannot list members": (m) => m === null,
268
+ });
269
+
270
+ const invitations = createInvitations(communityId, {
271
+ users: [{ userId: nonMemberUser.id, role: MembershipRole.MEMBER }],
272
+ });
273
+ check(invitations, {
274
+ "Non-member cannot create invitations": (i) => i === null,
275
+ });
276
+
277
+ const invitationsList = listCommunityInvitations(communityId);
278
+ check(invitationsList, {
279
+ "Non-member cannot list invitations": (i) => i === null,
280
+ });
281
+
282
+ // Test role update permission for non-members
283
+ const roleUpdate = updateMemberRole(
284
+ communityId,
285
+ memberUser.id,
286
+ MembershipRole.ADMIN,
287
+ );
288
+ check(roleUpdate, {
289
+ "Non-member cannot update member roles": (r) => r === null,
290
+ });
291
+
292
+ // Test global listing endpoint - should work for all users regardless of membership
293
+ const communityList = listCommunities();
294
+ check(communityList, {
295
+ "Non-member can list all communities": (c) => c !== null,
296
+ });
297
+
298
+ // Test leaving community - should fail for non-members
299
+ const leaveResult = leaveCommunity(communityId);
300
+ check(leaveResult, {
301
+ "Non-member cannot leave a community they're not part of": (r) =>
302
+ r !== true,
303
+ });
304
+
305
+ // Test removing members - should fail for non-members
306
+ const removeResult = removeMember(communityId, memberUser.id);
307
+ check(removeResult, {
308
+ "Non-member cannot remove other members": (r) => r !== true,
309
+ });
310
+
311
+ // Test invitation management - should fail for non-members
312
+ // First, we need an invitation to work with (will be created by admin later)
313
+ const myInvitations = listMyInvitations();
314
+ check(myInvitations, {
315
+ "Non-member can list their own invitations": (i) => i !== null,
316
+ });
317
+
318
+ const deleteInvResult = deleteInvitation(communityId, 9999); // Using dummy ID
319
+ check(deleteInvResult, {
320
+ "Non-member cannot delete invitations": (r) => r !== true,
321
+ });
322
+
323
+ const deleteBatchResult = deleteInvitationsBatch(communityId, [9999]);
324
+ check(deleteBatchResult, {
325
+ "Non-member cannot batch delete invitations": (r) => r !== true,
326
+ });
327
+
328
+ // Update invitation status should work for personal invitations, but non-member has none
329
+ // Will test this functionality with the invited user later
330
+
331
+ // Test batch removing members - should fail for non-members
332
+ const batchRemoveResult = removeMembers(communityId, [memberUser.id]);
333
+ check(batchRemoveResult, {
334
+ "Non-member cannot remove members in batch": (r) => r === null,
335
+ });
336
+ // Resource API tests for non-members
337
+ const resources = listCommunityResources(communityId);
338
+ check(resources, {
339
+ "Non-member cannot list community resources": (r) => r === null,
340
+ });
341
+
342
+ const resource = getResource(communityId, resourceId);
343
+ check(resource, {
344
+ "Non-member cannot get resource details": (r) => r === null,
345
+ });
346
+
347
+ const resourceCount = countResources(communityId);
348
+ check(resourceCount, {
349
+ "Non-member cannot count resources": (r) => r === null,
350
+ });
351
+
352
+ const newResource = createResource(communityId, {
353
+ type: ResourceType.ENT,
354
+ appName: AppName.EXTERNAL_LINK,
355
+ title: "Unauthorized Resource",
356
+ resourceUrl: "https://example.com/unauthorized",
357
+ openInNewTab: true,
358
+ });
359
+
360
+ check(newResource, {
361
+ "Non-member cannot create resources": (r) => r === null,
362
+ });
363
+
364
+ const updatedResource = updateResource(communityId, resourceId, {
365
+ title: "Updated by Non-member",
366
+ });
367
+
368
+ check(updatedResource, {
369
+ "Non-member cannot update resources": (r) => r === null,
370
+ });
371
+
372
+ const deleteResult = deleteResource(communityId, resourceId);
373
+
374
+ check(deleteResult, {
375
+ "Non-member cannot delete resources": (r) => r === false,
376
+ });
377
+
378
+ const cleanupResult = processMarkedResources(communityId);
379
+
380
+ check(cleanupResult, {
381
+ "Non-member cannot access admin-only cleanup endpoint": (r) =>
382
+ r === null,
383
+ });
384
+
385
+ logout();
386
+ });
387
+
388
+ // Test access with regular member
389
+ group("Test API access with regular member", () => {
390
+ authenticateWeb(memberUser.login, "password");
391
+
392
+ // Tests requiring MEMBER role (should succeed)
393
+ const community = getCommunity(String(communityId));
394
+ check(community, {
395
+ "Member can get community details": (c) => c !== null,
396
+ });
397
+
398
+ const stats = getCommunityStats(String(communityId));
399
+ check(stats, {
400
+ "Member can get community stats": (s) => s !== null,
401
+ });
402
+
403
+ const members = listCommunityMembers(communityId);
404
+ check(members, {
405
+ "Member can list members": (m) => m !== null,
406
+ });
407
+
408
+ // Tests requiring ADMIN role (should fail)
409
+ const secretCode = getSecretCode(communityId);
410
+ check(secretCode, {
411
+ "Member cannot get secret code (admin only)": (s) => s === null,
412
+ });
413
+
414
+ const updatedCommunity = updateCommunity(String(communityId), {
415
+ title: "Updated Title",
416
+ type: "CLASS",
417
+ });
418
+ check(updatedCommunity, {
419
+ "Member cannot update community (admin only)": (c) => c === null,
420
+ });
421
+
422
+ const welcomeNote = updateWelcomeNote(
423
+ String(communityId),
424
+ "New welcome note",
425
+ );
426
+ check(welcomeNote, {
427
+ "Member cannot update welcome note (admin only)": (w) => w === null,
428
+ });
429
+
430
+ const invitations = createInvitations(communityId, {
431
+ users: [{ userId: nonMemberUser.id, role: MembershipRole.MEMBER }],
432
+ });
433
+ check(invitations, {
434
+ "Member cannot create invitations (admin only)": (i) => i === null,
435
+ });
436
+
437
+ // Test role update permission for regular members
438
+ const roleUpdate = updateMemberRole(
439
+ communityId,
440
+ memberUser.id,
441
+ MembershipRole.ADMIN,
442
+ );
443
+ check(roleUpdate, {
444
+ "Regular member cannot promote themselves to admin": (r) => r === null,
445
+ });
446
+
447
+ // Test global listing endpoint
448
+ const communityList = listCommunities();
449
+ check(communityList, {
450
+ "Member can list all communities": (c) => c !== null,
451
+ });
452
+
453
+ // Test removing members - should fail for regular members
454
+ const removeResult = removeMember(communityId, memberUser.id);
455
+ check(removeResult, {
456
+ "Regular member cannot remove other members": (r) => r !== true,
457
+ });
458
+ // Test batch removing members - should fail for regular members
459
+ const batchRemoveResult = removeMembers(communityId, [memberUser.id]);
460
+ check(batchRemoveResult, {
461
+ "Regular member cannot remove members in batch (admin only)": (r) =>
462
+ r === null,
463
+ });
464
+ // Test invitation management - should fail for regular members
465
+ const myInvitations = listMyInvitations();
466
+ check(myInvitations, {
467
+ "Member can list their own invitations": (i) => i !== null,
468
+ });
469
+
470
+ const deleteInvResult = deleteInvitation(communityId, 9999); // Using dummy ID
471
+ check(deleteInvResult, {
472
+ "Regular member cannot delete invitations": (r) => r !== true,
473
+ });
474
+
475
+ const deleteBatchResult = deleteInvitationsBatch(communityId, [9999]);
476
+ check(deleteBatchResult, {
477
+ "Regular member cannot batch delete invitations": (r) => r !== true,
478
+ });
479
+
480
+ // Resource API tests for members (read-only access)
481
+ const resources = listCommunityResources(communityId);
482
+ check(resources, {
483
+ "Member can list community resources": (r) => r !== null,
484
+ "Resource list contains test resource": (r) => {
485
+ if (!r || !r.items) return false;
486
+ return r.items.some((item) => item.id === resourceId);
487
+ },
488
+ });
489
+
490
+ const resource = getResource(communityId, resourceId);
491
+ check(resource, {
492
+ "Member can get resource details": (r) => r !== null,
493
+ "Resource details are correct": (r) =>
494
+ r !== null && r.id === resourceId,
495
+ });
496
+
497
+ const resourceCount = countResources(communityId);
498
+ check(resourceCount, {
499
+ "Member can count resources": (r) => r !== null,
500
+ "Resource count returns positive number": (r) =>
501
+ r !== null && r.count > 0,
502
+ });
503
+
504
+ // Tests requiring ADMIN role (should fail)
505
+ const newResource = createResource(communityId, {
506
+ type: ResourceType.ENT,
507
+ appName: AppName.EXTERNAL_LINK,
508
+ title: "Member Resource Attempt",
509
+ resourceUrl: "https://example.com/member-attempt",
510
+ openInNewTab: true,
511
+ });
512
+
513
+ check(newResource, {
514
+ "Member cannot create resources (admin only)": (r) => r === null,
515
+ });
516
+
517
+ const updatedResource = updateResource(communityId, resourceId, {
518
+ title: "Updated by Member",
519
+ });
520
+
521
+ check(updatedResource, {
522
+ "Member cannot update resources (admin only)": (r) => r === null,
523
+ });
524
+
525
+ const deleteResult = deleteResource(communityId, resourceId);
526
+
527
+ check(deleteResult, {
528
+ "Member cannot delete resources (admin only)": (r) => r === false,
529
+ });
530
+
531
+ const cleanupResult = processMarkedResources(communityId);
532
+
533
+ check(cleanupResult, {
534
+ "Member cannot access admin-only cleanup endpoint": (r) => r === null,
535
+ });
536
+
537
+ // Leave community - should succeed for members
538
+ // We do this last as it changes member's status
539
+ const leaveResult = leaveCommunity(communityId);
540
+ check(leaveResult, {
541
+ "Member can leave a community": (r) => r === true,
542
+ });
543
+
544
+ // Verify the member actually left
545
+ authenticateWeb(adminUser.login, "password");
546
+ const membersAfterLeaving = listCommunityMembers(communityId);
547
+ logout();
548
+ authenticateWeb(memberUser.login, "password"); // Log back in as member
549
+
550
+ if (membersAfterLeaving && membersAfterLeaving.items) {
551
+ const memberStillExists = membersAfterLeaving.items.some(
552
+ (member) => member.user.entId === memberUser.id,
553
+ );
554
+ check(memberStillExists, {
555
+ "Member no longer appears in community member list": (exists) =>
556
+ !exists,
557
+ });
558
+ }
559
+
560
+ logout();
561
+ });
562
+
563
+ // Test access with admin user
564
+ group("Test API access with admin user", () => {
565
+ authenticateWeb(adminUser.login, "password");
566
+
567
+ // All operations should succeed for admin
568
+
569
+ // Member-level operations
570
+ const community = getCommunity(String(communityId));
571
+ check(community, {
572
+ "Admin can get community details": (c) => c !== null,
573
+ });
574
+
575
+ const stats = getCommunityStats(String(communityId));
576
+ check(stats, {
577
+ "Admin can get community stats": (s) => s !== null,
578
+ });
579
+
580
+ const members = listCommunityMembers(communityId);
581
+ check(members, {
582
+ "Admin can list members": (m) => m !== null,
583
+ });
584
+
585
+ // Admin-level operations
586
+ const secretCode = getSecretCode(communityId);
587
+ check(secretCode, {
588
+ "Admin can get secret code": (s) => s !== null,
589
+ });
590
+
591
+ const updatedCommunity = updateCommunity(String(communityId), {
592
+ title: "Admin Updated Title",
593
+ type: "CLASS",
594
+ });
595
+ check(updatedCommunity, {
596
+ "Admin can update community": (c) => c !== null,
597
+ });
598
+
599
+ const welcomeNote = updateWelcomeNote(
600
+ String(communityId),
601
+ "Admin updated welcome note",
602
+ );
603
+ check(welcomeNote, {
604
+ "Admin can update welcome note": (w) => w !== null,
605
+ });
606
+
607
+ const invitations = createInvitations(communityId, {
608
+ users: [{ userId: nonMemberUser.id, role: MembershipRole.MEMBER }],
609
+ message: "Please join our community",
610
+ });
611
+ check(invitations, {
612
+ "Admin can create invitations": (i) => i !== null,
613
+ });
614
+ const userId =
615
+ invitations && invitations.length > 0
616
+ ? invitations[0].receiver?.id
617
+ : null;
618
+ check(userId, {
619
+ "Admin created an invitation for a user": (i) => i != null,
620
+ });
621
+
622
+ // The invited user must accept the invitation before we can update their role
623
+ // First save invitation details
624
+ const tmpInvitationId =
625
+ invitations && invitations.length > 0 ? invitations[0].id : null;
626
+
627
+ // Switch to the invited user to accept the invitation
628
+ logout();
629
+ authenticateWeb(nonMemberUser.login, "password");
630
+
631
+ // Accept the invitation
632
+ if (tmpInvitationId) {
633
+ const acceptResult = updateInvitationStatus(
634
+ tmpInvitationId,
635
+ InvitationStatus.ACCEPTED,
636
+ );
637
+
638
+ check(acceptResult, {
639
+ "Non-member successfully accepted invitation": (r) => r !== null,
640
+ });
641
+
642
+ // Verify the user is now a member
643
+ const membershipCheck = getCommunity(String(communityId));
644
+ check(membershipCheck, {
645
+ "User is now a community member after accepting invitation": (m) =>
646
+ m !== null,
647
+ });
648
+ }
649
+
650
+ // Switch back to admin
651
+ logout();
652
+ authenticateWeb(adminUser.login, "password");
653
+
654
+ // Now that the user is a member, we can update their role
655
+ const roleUpdate = updateMemberRole(
656
+ communityId,
657
+ userId!,
658
+ MembershipRole.ADMIN,
659
+ );
660
+ check(roleUpdate, {
661
+ "Admin can update member roles": (r) => r !== null,
662
+ });
663
+
664
+ // Verify the member was successfully promoted
665
+ const updatedMembers = listCommunityMembers(communityId);
666
+ if (updatedMembers && updatedMembers.items) {
667
+ const promotedMember = updatedMembers.items.find(
668
+ (member) => member.user.id === userId,
669
+ );
670
+ check(promotedMember, {
671
+ "Member was successfully promoted to admin": (m) =>
672
+ !!m && m.role === MembershipRole.ADMIN,
673
+ });
674
+ }
675
+
676
+ // Test global listing endpoint
677
+ const communityList = listCommunities();
678
+ check(communityList, {
679
+ "Admin can list all communities": (c) => c !== null,
680
+ });
681
+
682
+ // Test invitation operations for admin
683
+ // Create separate invitations specifically for testing deletion
684
+ // These will remain in PENDING status
685
+ const deletionTestInvitations = createInvitations(communityId, {
686
+ users: [{ userId: memberUser.id, role: MembershipRole.MEMBER }],
687
+ message: "Invitation for testing deletion (will remain pending)",
688
+ });
689
+
690
+ // Verify the test invitations were created
691
+ check(deletionTestInvitations, {
692
+ "Admin created pending invitations for deletion testing": (i) =>
693
+ i !== null && i.length > 0,
694
+ });
695
+
696
+ // Now test deletion with a pending invitation
697
+ if (deletionTestInvitations && deletionTestInvitations.length > 0) {
698
+ const pendingInvitationId = deletionTestInvitations[0].id;
699
+
700
+ // Test deleting a specific invitation
701
+ const deleteInvResult = deleteInvitation(
702
+ communityId,
703
+ pendingInvitationId,
704
+ );
705
+ check(deleteInvResult, {
706
+ "Admin can delete a specific pending invitation": (r) => r === true,
707
+ });
708
+
709
+ // Create more pending invitations for batch delete test
710
+ const batchInvitations = createInvitations(communityId, {
711
+ users: [
712
+ { userId: memberUser.id, role: MembershipRole.MEMBER },
713
+ { userId: memberUser.id, role: MembershipRole.ADMIN },
714
+ ],
715
+ message: "Batch deletion test (pending invitations)",
716
+ });
717
+
718
+ if (batchInvitations && batchInvitations.length > 0) {
719
+ const pendingInvitationIds = batchInvitations.map((inv) => inv.id);
720
+
721
+ // Test batch deletion of pending invitations
722
+ const batchDeleteResult = deleteInvitationsBatch(
723
+ communityId,
724
+ pendingInvitationIds,
725
+ );
726
+ check(batchDeleteResult, {
727
+ "Admin can batch delete pending invitations": (r) => r === true,
728
+ });
729
+ }
730
+ }
731
+
732
+ // Test member removal
733
+ // First, we need a new member to remove
734
+ authenticateWeb(nonMemberUser.login, "password");
735
+ joinCommunity(communitySecretCode);
736
+ logout();
737
+ authenticateWeb(adminUser.login, "password"); // Log back in as admin
738
+ // Re-fetch the community to get the latest member list
739
+ const listMembers = listCommunityMembers(communityId);
740
+ check(listMembers, {
741
+ "Admin can list community members": (m) => m !== null,
742
+ });
743
+ check(listMembers?.items, {
744
+ "Community has members to remove": (m) => m !== null && m!.length > 0,
745
+ });
746
+ // Test removing a member
747
+ const removeResult = removeMember(
748
+ communityId,
749
+ listMembers?.items[0].user.id!,
750
+ );
751
+ check(removeResult, {
752
+ "Admin can remove a member from the community": (r) => r === true,
753
+ });
754
+
755
+ const batchRemoveResult = removeMembers(communityId, [
756
+ listMembers?.items[0].user.id!,
757
+ ]);
758
+ check(batchRemoveResult, {
759
+ "Admin can remove members in batch (admin only)": (r) => r !== null,
760
+ });
761
+ // Verify the member was removed
762
+ const membersAfterRemoval = listCommunityMembers(communityId);
763
+ if (membersAfterRemoval && membersAfterRemoval.items) {
764
+ const removedMemberStillExists = membersAfterRemoval.items.some(
765
+ (member) => member.user.entId === nonMemberUser.id,
766
+ );
767
+ check(removedMemberStillExists, {
768
+ "Member was successfully removed from community": (exists) => !exists,
769
+ });
770
+ }
771
+
772
+ // Admin should be able to view their own invitations
773
+ const myInvitations = listMyInvitations();
774
+ check(myInvitations, {
775
+ "Admin can list their own invitations": (i) => i !== null,
776
+ });
777
+
778
+ sleep(1); // Small pause before cleanup
779
+
780
+ // Cleanup - delete the community
781
+ const deleted = deleteCommunity(communityId);
782
+ check(deleted, {
783
+ "Admin can delete the community": (r) => r === true,
784
+ });
785
+
786
+ logout();
787
+ });
788
+
789
+ // Test invitation acceptance/rejection flow
790
+ group("Test invitation management with invited user", () => {
791
+ // Admin creates an invitation
792
+ authenticateWeb(adminUser.login, "password");
793
+
794
+ // We need a new community for this test to avoid conflicts
795
+ const inviteCommunityId = Number(
796
+ createCommunityOrFail({
797
+ title: "Invitation Test Community",
798
+ type: "CLASS",
799
+ schoolYearStart: 2025,
800
+ schoolYearEnd: 2026,
801
+ }),
802
+ );
803
+
804
+ // Create invitation for non-member
805
+ const invitation = createInvitations(inviteCommunityId, {
806
+ users: [{ userId: nonMemberUser.id, role: MembershipRole.MEMBER }],
807
+ message: "Please test this invitation",
808
+ });
809
+
810
+ check(invitation, {
811
+ "Admin created invitation for user": (i) => i !== null && i.length > 0,
812
+ });
813
+
814
+ if (invitation && invitation.length > 0) {
815
+ // Non-member checks and updates their invitation
816
+ logout();
817
+ authenticateWeb(nonMemberUser.login, "password");
818
+
819
+ // Check user can list their invitations
820
+ const pendingInvitations = listMyInvitations();
821
+ check(pendingInvitations, {
822
+ "Invited user can list their pending invitations": (i) =>
823
+ i !== null && i.items && i.items.length > 0,
824
+ });
825
+
826
+ // Find our test invitation
827
+ let testInvitation;
828
+ if (pendingInvitations && pendingInvitations.items) {
829
+ testInvitation = pendingInvitations.items.find(
830
+ (inv) => Number(inv.communityId) === inviteCommunityId,
831
+ );
832
+ }
833
+
834
+ check(testInvitation, {
835
+ "Invitation appears in user's list": (i) => i !== undefined,
836
+ });
837
+
838
+ if (testInvitation) {
839
+ // Accept the invitation
840
+ const updateResult = updateInvitationStatus(
841
+ testInvitation.id,
842
+ InvitationStatus.ACCEPTED,
843
+ );
844
+
845
+ check(updateResult, {
846
+ "User can accept their own invitation": (r) => r !== null,
847
+ });
848
+
849
+ // Verify user is now a member
850
+ const membership = getCommunity(String(inviteCommunityId));
851
+ check(membership, {
852
+ "User can now access the community after accepting invitation": (
853
+ m,
854
+ ) => m !== null,
855
+ });
856
+ }
857
+
858
+ logout();
859
+ authenticateWeb(adminUser.login, "password");
860
+
861
+ // Clean up this test community
862
+ deleteCommunity(inviteCommunityId);
863
+ }
864
+
865
+ logout();
866
+ });
867
+ });
868
+ }