@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.
- package/it/scenarios/api-community-oauth.spec.ts +106 -0
- package/it/scenarios/api-community-role-check.spec.ts +868 -0
- package/it/scenarios/api-community.spec.ts +89 -0
- package/it/scenarios/api-invitations-search-sort.spec.ts +858 -0
- package/it/scenarios/api-invitations.spec.ts +582 -0
- package/it/scenarios/api-membership.spec.ts +411 -0
- package/it/scenarios/api-permission-check.spec.ts +424 -0
- package/it/scenarios/api-resources-search-sort.spec.ts +600 -0
- package/it/scenarios/api-resources.spec.ts +183 -0
- package/it/scenarios/utils/_community-api.utils.ts +317 -0
- package/it/scenarios/utils/_community-tests.utils.ts +83 -0
- package/it/scenarios/utils/_invitation-api.utils.ts +453 -0
- package/it/scenarios/utils/_membership-api.utils.ts +184 -0
- package/it/scenarios/utils/_resource-api.utils.ts +292 -0
- package/it/scenarios/utils/_resource-ent.utils.ts +415 -0
- package/it/scenarios/utils/_resource-tests.utils.ts +396 -0
- package/it/scenarios/utils/_role.utils.ts +33 -0
- package/loadtest/index.ts +6 -0
- package/package.json +48 -0
- package/vite.config.ts +23 -0
|
@@ -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
|
+
}
|