@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,858 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import { chai, describe } from "https://jslib.k6.io/k6chaijs/4.3.4.0/index.js";
|
|
3
|
+
import { fail, check } from "k6";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
authenticateWeb,
|
|
7
|
+
getUsersOfSchool,
|
|
8
|
+
createAndSetRole,
|
|
9
|
+
linkRoleToUsers,
|
|
10
|
+
initStructure,
|
|
11
|
+
getRandomUserWithProfile,
|
|
12
|
+
getRolesOfStructure,
|
|
13
|
+
Structure,
|
|
14
|
+
logout,
|
|
15
|
+
UserInfo,
|
|
16
|
+
} from "../../node_modules/edifice-k6-commons/dist/index.js";
|
|
17
|
+
|
|
18
|
+
// Import community API utils
|
|
19
|
+
import {
|
|
20
|
+
createCommunityOrFail,
|
|
21
|
+
deleteCommunity,
|
|
22
|
+
CreateCommunityParams,
|
|
23
|
+
} from "./utils/_community-api.utils.ts";
|
|
24
|
+
|
|
25
|
+
// Import invitation API utils
|
|
26
|
+
import {
|
|
27
|
+
createInvitations,
|
|
28
|
+
listCommunityInvitations,
|
|
29
|
+
updateInvitationStatus,
|
|
30
|
+
CreateInvitationParams,
|
|
31
|
+
Invitation,
|
|
32
|
+
InvitationStatus,
|
|
33
|
+
MembershipRole,
|
|
34
|
+
InvitationSortField,
|
|
35
|
+
SortDirection,
|
|
36
|
+
updateLastVisit,
|
|
37
|
+
CommunitySection,
|
|
38
|
+
} from "./utils/_invitation-api.utils.ts";
|
|
39
|
+
|
|
40
|
+
chai.config.logFailures = true;
|
|
41
|
+
|
|
42
|
+
export const options = {
|
|
43
|
+
setupTimeout: "1h",
|
|
44
|
+
maxRedirects: 0,
|
|
45
|
+
thresholds: {
|
|
46
|
+
checks: ["rate == 1.00"],
|
|
47
|
+
},
|
|
48
|
+
scenarios: {
|
|
49
|
+
invitationsSearchSortTest: {
|
|
50
|
+
exec: "testInvitationsSearchSort",
|
|
51
|
+
executor: "per-vu-iterations",
|
|
52
|
+
vus: 1,
|
|
53
|
+
maxDuration: "1m",
|
|
54
|
+
gracefulStop: "5s",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const timestamp = new Date().toISOString();
|
|
60
|
+
const schoolName = `IT Community Invitations Search Sort`;
|
|
61
|
+
|
|
62
|
+
export function setup(): Structure {
|
|
63
|
+
let structure: Structure = {} as Structure;
|
|
64
|
+
describe("[CommunityInvitations] Initialize data", () => {
|
|
65
|
+
authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD);
|
|
66
|
+
structure = initStructure(schoolName, "tiny");
|
|
67
|
+
const role = createAndSetRole("Communities");
|
|
68
|
+
const groups = getRolesOfStructure(structure.id);
|
|
69
|
+
linkRoleToUsers(
|
|
70
|
+
structure,
|
|
71
|
+
role,
|
|
72
|
+
groups.map((g: any) => g.name),
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
return structure;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function testInvitationsSearchSort(structure: Structure) {
|
|
79
|
+
// Community data
|
|
80
|
+
const communityData: CreateCommunityParams = {
|
|
81
|
+
title: `InvitationSearchSort - Test ${timestamp}`,
|
|
82
|
+
type: "FREE",
|
|
83
|
+
schoolYearStart: 2025,
|
|
84
|
+
schoolYearEnd: 2026,
|
|
85
|
+
discussionEnabled: true,
|
|
86
|
+
welcomeNote: "Test community for invitation search and sort API tests",
|
|
87
|
+
invitations: {
|
|
88
|
+
users: [],
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
let communityId: number;
|
|
93
|
+
let invitationId: number;
|
|
94
|
+
let adminUser: UserInfo;
|
|
95
|
+
let memberUser: UserInfo;
|
|
96
|
+
let member2User: UserInfo;
|
|
97
|
+
let member3User: UserInfo;
|
|
98
|
+
|
|
99
|
+
describe("[Community Invitations Search and Sort Test]", () => {
|
|
100
|
+
// Initial setup - Get users and authenticate admin
|
|
101
|
+
authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD);
|
|
102
|
+
|
|
103
|
+
// Get users from school
|
|
104
|
+
const users = getUsersOfSchool(structure);
|
|
105
|
+
adminUser = getRandomUserWithProfile(users, "Teacher");
|
|
106
|
+
memberUser = getRandomUserWithProfile(users, "Student", [adminUser]);
|
|
107
|
+
member2User = getRandomUserWithProfile(users, "Student", [
|
|
108
|
+
adminUser,
|
|
109
|
+
memberUser,
|
|
110
|
+
]);
|
|
111
|
+
member3User = getRandomUserWithProfile(users, "Student", [
|
|
112
|
+
adminUser,
|
|
113
|
+
memberUser,
|
|
114
|
+
member2User,
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
logout();
|
|
118
|
+
|
|
119
|
+
// Admin authenticates
|
|
120
|
+
const adminAuthenticated = authenticateWeb(adminUser.login, "password");
|
|
121
|
+
if (!adminAuthenticated) {
|
|
122
|
+
fail("Admin authentication failed");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Create a community for testing
|
|
126
|
+
describe("Create community and invitations for search/sort tests", () => {
|
|
127
|
+
// Create community
|
|
128
|
+
communityId = Number(createCommunityOrFail(communityData));
|
|
129
|
+
console.log(`Created community with ID: ${communityId}`);
|
|
130
|
+
|
|
131
|
+
// Create first invitation (will be accepted later)
|
|
132
|
+
const invitationData: CreateInvitationParams = {
|
|
133
|
+
users: [{ userId: memberUser.id, role: MembershipRole.MEMBER }],
|
|
134
|
+
message: "You are invited to join our test community",
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const invitations = createInvitations(communityId, invitationData);
|
|
138
|
+
invitationId = invitations![0].id;
|
|
139
|
+
console.log(`Created invitation with ID: ${invitationId}`);
|
|
140
|
+
|
|
141
|
+
// Create additional invitations for different users to test search/sort
|
|
142
|
+
for (let i = 0; i < 2; i++) {
|
|
143
|
+
const additionalData: CreateInvitationParams = {
|
|
144
|
+
users: [
|
|
145
|
+
{
|
|
146
|
+
userId: i === 0 ? member2User.id : member3User.id,
|
|
147
|
+
role: i === 0 ? MembershipRole.MEMBER : MembershipRole.ADMIN,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
message: `Test invitation ${i + 1} for search/sort`,
|
|
151
|
+
};
|
|
152
|
+
createInvitations(communityId, additionalData);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Have the first user accept their invitation to ensure mixed statuses
|
|
157
|
+
describe("Member accepts invitation to create mixed statuses", () => {
|
|
158
|
+
// Member logs in
|
|
159
|
+
logout();
|
|
160
|
+
authenticateWeb(memberUser.login, "password");
|
|
161
|
+
|
|
162
|
+
// Accept invitation
|
|
163
|
+
updateInvitationStatus(invitationId, InvitationStatus.ACCEPTED);
|
|
164
|
+
|
|
165
|
+
// Log back in as admin
|
|
166
|
+
logout();
|
|
167
|
+
authenticateWeb(adminUser.login, "password");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Test user search with ILIKE on displayName
|
|
171
|
+
describe("Admin searches for invitations by user displayName", () => {
|
|
172
|
+
// First, we need to get the user's display name to search for
|
|
173
|
+
const memberDisplayName = `${memberUser.lastName} ${memberUser.firstName}`;
|
|
174
|
+
console.log(
|
|
175
|
+
`Searching for invitations with user displayName containing: ${memberDisplayName}`,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Try searching with full displayName
|
|
179
|
+
const fullNameSearch = listCommunityInvitations(communityId, {
|
|
180
|
+
searchTerm: memberDisplayName,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
check(fullNameSearch, {
|
|
184
|
+
"can search invitations by full displayName": (r) =>
|
|
185
|
+
r !== null && r.length > 0,
|
|
186
|
+
"search results contain the right invitation": (r) => {
|
|
187
|
+
return (
|
|
188
|
+
r !== null &&
|
|
189
|
+
r.some(
|
|
190
|
+
(inv: Invitation) =>
|
|
191
|
+
inv.id === invitationId &&
|
|
192
|
+
inv.receiver?.displayName === memberDisplayName,
|
|
193
|
+
)
|
|
194
|
+
);
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Try searching with partial displayName (if displayName has multiple parts)
|
|
199
|
+
if (memberDisplayName && memberDisplayName.includes(" ")) {
|
|
200
|
+
// Take just the first part of the name (typically last name)
|
|
201
|
+
const partialName = memberDisplayName.split(" ")[0];
|
|
202
|
+
|
|
203
|
+
const partialNameSearch = listCommunityInvitations(communityId, {
|
|
204
|
+
searchTerm: partialName,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
check(partialNameSearch, {
|
|
208
|
+
"can search invitations by partial displayName": (r) =>
|
|
209
|
+
r !== null && r.length > 0,
|
|
210
|
+
"partial name search finds correct invitation": (r) => {
|
|
211
|
+
return (
|
|
212
|
+
r !== null &&
|
|
213
|
+
r.some(
|
|
214
|
+
(inv: Invitation) =>
|
|
215
|
+
inv.id === invitationId &&
|
|
216
|
+
inv.receiver?.displayName.includes(partialName),
|
|
217
|
+
)
|
|
218
|
+
);
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Test search with lowercase (testing case insensitivity)
|
|
224
|
+
if (memberDisplayName) {
|
|
225
|
+
const lowercaseSearch = listCommunityInvitations(communityId, {
|
|
226
|
+
searchTerm: memberDisplayName.toLowerCase(),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
check(lowercaseSearch, {
|
|
230
|
+
"search is case-insensitive": (r) => r !== null && r.length > 0,
|
|
231
|
+
"case-insensitive search finds correct invitation": (r) => {
|
|
232
|
+
return (
|
|
233
|
+
r !== null && r.some((inv: Invitation) => inv.id === invitationId)
|
|
234
|
+
);
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Test with a non-matching search term
|
|
240
|
+
const nonMatchingSearch = listCommunityInvitations(communityId, {
|
|
241
|
+
searchTerm: "ThisUserDoesNotExist12345",
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
check(nonMatchingSearch, {
|
|
245
|
+
"non-matching search returns empty results": (r) =>
|
|
246
|
+
r !== null && r.length === 0,
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Test sorting capabilities
|
|
251
|
+
describe("Admin tests invitation sorting capabilities", () => {
|
|
252
|
+
// First create multiple invitations to ensure we have data for meaningful sorting
|
|
253
|
+
console.log("Creating additional invitations for sorting tests");
|
|
254
|
+
|
|
255
|
+
// Send multiple invitations for sorting tests
|
|
256
|
+
const sortTestUsers = [member2User, member3User];
|
|
257
|
+
|
|
258
|
+
// Track the new invitation IDs
|
|
259
|
+
const sortInvitationIds: number[] = [];
|
|
260
|
+
|
|
261
|
+
// Create invitations with different messages to sort on
|
|
262
|
+
for (let i = 0; i < sortTestUsers.length; i++) {
|
|
263
|
+
const sortInvitationData: CreateInvitationParams = {
|
|
264
|
+
users: [{ userId: sortTestUsers[i].id, role: MembershipRole.MEMBER }],
|
|
265
|
+
message: `Sort test invitation ${String.fromCharCode(65 + i)}`, // "A", "B", etc.
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const invs = createInvitations(communityId, sortInvitationData);
|
|
269
|
+
if (invs && invs.length > 0) {
|
|
270
|
+
sortInvitationIds.push(invs[0].id);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Test sorting
|
|
275
|
+
describe("Sort invitations ", () => {
|
|
276
|
+
// Test sorting by status
|
|
277
|
+
const statusSortAsc = listCommunityInvitations(communityId, {
|
|
278
|
+
sortBy: InvitationSortField.STATUS,
|
|
279
|
+
sortDirection: SortDirection.ASC,
|
|
280
|
+
});
|
|
281
|
+
// We are sorting according workflow order, not alphabetically
|
|
282
|
+
|
|
283
|
+
check(statusSortAsc, {
|
|
284
|
+
"sort by status ASC works": (r) => r !== null && r.length > 0,
|
|
285
|
+
|
|
286
|
+
// Verify enum order using JavaScript simulation of PostgreSQL enum ordering
|
|
287
|
+
"Status ASC sort follows PostgreSQL enum definition order": (r) => {
|
|
288
|
+
if (!r || r.length <= 1) return true;
|
|
289
|
+
|
|
290
|
+
// Define expected order based on enum definition in database
|
|
291
|
+
const enumOrder = {
|
|
292
|
+
[InvitationStatus.PENDING]: 1, // First position in enum
|
|
293
|
+
[InvitationStatus.REQUEST]: 2, // Second position
|
|
294
|
+
[InvitationStatus.ACCEPTED]: 3, // Third position
|
|
295
|
+
[InvitationStatus.REJECTED]: 4, // Fourth position
|
|
296
|
+
[InvitationStatus.REQUEST_ACCEPTED]: 5, // Fifth position
|
|
297
|
+
[InvitationStatus.REQUEST_REJECTED]: 6, // Sixth position
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// Create a manual sort using the enum order
|
|
301
|
+
const manualSort = [...r].sort((a, b) => {
|
|
302
|
+
return enumOrder[a.status] - enumOrder[b.status];
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Check if API results match our manual sort
|
|
306
|
+
const orderMatches = r.every(
|
|
307
|
+
(invitation, index) => invitation.id === manualSort[index].id,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
if (!orderMatches) {
|
|
311
|
+
console.log(
|
|
312
|
+
"Warning: Status ASC order does not match expected enum order",
|
|
313
|
+
);
|
|
314
|
+
console.log(
|
|
315
|
+
"Expected order:",
|
|
316
|
+
manualSort.map((inv) => inv.status),
|
|
317
|
+
);
|
|
318
|
+
console.log(
|
|
319
|
+
"Actual order:",
|
|
320
|
+
r.map((inv) => inv.status),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return orderMatches;
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
// Check specific status order relationships if we have multiple statuses
|
|
328
|
+
"Status values are correctly ordered in ASC sort": (r) => {
|
|
329
|
+
if (!r || r.length <= 1) return true;
|
|
330
|
+
|
|
331
|
+
// Get indexes of various statuses
|
|
332
|
+
const pendingIndex = r.findIndex(
|
|
333
|
+
(inv) => inv.status === InvitationStatus.PENDING,
|
|
334
|
+
);
|
|
335
|
+
const requestIndex = r.findIndex(
|
|
336
|
+
(inv) => inv.status === InvitationStatus.REQUEST,
|
|
337
|
+
);
|
|
338
|
+
const acceptedIndex = r.findIndex(
|
|
339
|
+
(inv) => inv.status === InvitationStatus.ACCEPTED,
|
|
340
|
+
);
|
|
341
|
+
const rejectedIndex = r.findIndex(
|
|
342
|
+
(inv) => inv.status === InvitationStatus.REJECTED,
|
|
343
|
+
);
|
|
344
|
+
const requestAcceptedIndex = r.findIndex(
|
|
345
|
+
(inv) => inv.status === InvitationStatus.REQUEST_ACCEPTED,
|
|
346
|
+
);
|
|
347
|
+
const requestRejectedIndex = r.findIndex(
|
|
348
|
+
(inv) => inv.status === InvitationStatus.REQUEST_REJECTED,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// Check all possible pairs that exist in the results
|
|
352
|
+
// PENDING should come before all others
|
|
353
|
+
if (pendingIndex !== -1) {
|
|
354
|
+
if (requestIndex !== -1 && pendingIndex > requestIndex)
|
|
355
|
+
return false;
|
|
356
|
+
if (acceptedIndex !== -1 && pendingIndex > acceptedIndex)
|
|
357
|
+
return false;
|
|
358
|
+
if (rejectedIndex !== -1 && pendingIndex > rejectedIndex)
|
|
359
|
+
return false;
|
|
360
|
+
if (
|
|
361
|
+
requestAcceptedIndex !== -1 &&
|
|
362
|
+
pendingIndex > requestAcceptedIndex
|
|
363
|
+
)
|
|
364
|
+
return false;
|
|
365
|
+
if (
|
|
366
|
+
requestRejectedIndex !== -1 &&
|
|
367
|
+
pendingIndex > requestRejectedIndex
|
|
368
|
+
)
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// REQUEST should come after PENDING but before others
|
|
373
|
+
if (requestIndex !== -1) {
|
|
374
|
+
if (acceptedIndex !== -1 && requestIndex > acceptedIndex)
|
|
375
|
+
return false;
|
|
376
|
+
if (rejectedIndex !== -1 && requestIndex > rejectedIndex)
|
|
377
|
+
return false;
|
|
378
|
+
if (
|
|
379
|
+
requestAcceptedIndex !== -1 &&
|
|
380
|
+
requestIndex > requestAcceptedIndex
|
|
381
|
+
)
|
|
382
|
+
return false;
|
|
383
|
+
if (
|
|
384
|
+
requestRejectedIndex !== -1 &&
|
|
385
|
+
requestIndex > requestRejectedIndex
|
|
386
|
+
)
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
return true;
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Verify we get the same results for DESC
|
|
394
|
+
const statusSortDesc = listCommunityInvitations(communityId, {
|
|
395
|
+
sortBy: InvitationSortField.STATUS,
|
|
396
|
+
sortDirection: SortDirection.DESC,
|
|
397
|
+
});
|
|
398
|
+
// We are sorting according workflow order, not alphabetically
|
|
399
|
+
check(statusSortDesc, {
|
|
400
|
+
"sort by status DESC works": (r) => r !== null && r.length > 0,
|
|
401
|
+
|
|
402
|
+
// Verify enum order using JavaScript simulation of PostgreSQL DESC enum ordering
|
|
403
|
+
"Status DESC sort follows reverse PostgreSQL enum definition order": (
|
|
404
|
+
r,
|
|
405
|
+
) => {
|
|
406
|
+
if (!r || r.length <= 1) return true;
|
|
407
|
+
|
|
408
|
+
// Define expected order based on enum definition in database
|
|
409
|
+
const enumOrder = {
|
|
410
|
+
[InvitationStatus.PENDING]: 1,
|
|
411
|
+
[InvitationStatus.REQUEST]: 2,
|
|
412
|
+
[InvitationStatus.ACCEPTED]: 3,
|
|
413
|
+
[InvitationStatus.REJECTED]: 4,
|
|
414
|
+
[InvitationStatus.REQUEST_ACCEPTED]: 5,
|
|
415
|
+
[InvitationStatus.REQUEST_REJECTED]: 6,
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// Create a manual sort using the enum order (reversed for DESC)
|
|
419
|
+
const manualSort = [...r].sort((a, b) => {
|
|
420
|
+
return enumOrder[b.status] - enumOrder[a.status];
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Check if API results match our manual sort
|
|
424
|
+
const orderMatches = r.every(
|
|
425
|
+
(invitation, index) => invitation.id === manualSort[index].id,
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
if (!orderMatches) {
|
|
429
|
+
console.log(
|
|
430
|
+
"Warning: Status DESC order does not match expected enum order",
|
|
431
|
+
);
|
|
432
|
+
console.log(
|
|
433
|
+
"Expected order:",
|
|
434
|
+
manualSort.map((inv) => inv.status),
|
|
435
|
+
);
|
|
436
|
+
console.log(
|
|
437
|
+
"Actual order:",
|
|
438
|
+
r.map((inv) => inv.status),
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return orderMatches;
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Test sorting by userName
|
|
447
|
+
const userNameSortAsc = listCommunityInvitations(communityId, {
|
|
448
|
+
sortBy: InvitationSortField.USER_NAME,
|
|
449
|
+
sortDirection: SortDirection.ASC,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
check(userNameSortAsc, {
|
|
453
|
+
"sort by userName ASC works": (r) => r !== null && r.length > 0,
|
|
454
|
+
// Add JavaScript-based validation of server-side sorting
|
|
455
|
+
"API userName ASC sort matches JavaScript sort": (r) => {
|
|
456
|
+
if (!r || r.length <= 1) return true; // Nothing to validate with 0-1 items
|
|
457
|
+
|
|
458
|
+
// Create a copy and sort manually by displayName using JavaScript's sort
|
|
459
|
+
const manualSort = [...r].sort((a, b) => {
|
|
460
|
+
const nameA = a.receiver?.displayName || "";
|
|
461
|
+
const nameB = b.receiver?.displayName || "";
|
|
462
|
+
return nameA.localeCompare(nameB); // Use localeCompare for proper string comparison
|
|
463
|
+
});
|
|
464
|
+
// Log the original and manually sorted displayNames for debugging
|
|
465
|
+
console.log(
|
|
466
|
+
"API sorted names:",
|
|
467
|
+
r.map((inv) => inv.receiver?.displayName),
|
|
468
|
+
);
|
|
469
|
+
console.log(
|
|
470
|
+
"JS sorted names:",
|
|
471
|
+
manualSort.map((inv) => inv.receiver?.displayName),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
// Check if the order matches by comparing IDs at each position
|
|
475
|
+
const orderMatches = r.every(
|
|
476
|
+
(invitation, index) => invitation.id === manualSort[index].id,
|
|
477
|
+
);
|
|
478
|
+
if (!orderMatches) {
|
|
479
|
+
console.log(
|
|
480
|
+
"Warning: API sort order does not match JavaScript sort order",
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return orderMatches;
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const userNameSortDesc = listCommunityInvitations(communityId, {
|
|
488
|
+
sortBy: InvitationSortField.USER_NAME,
|
|
489
|
+
sortDirection: SortDirection.DESC,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
check(userNameSortDesc, {
|
|
493
|
+
"sort by userName DESC works": (r) => r !== null && r.length > 0,
|
|
494
|
+
"userName DESC returns different order than ASC": (r) => {
|
|
495
|
+
// This only works if we have multiple results
|
|
496
|
+
if (
|
|
497
|
+
!r ||
|
|
498
|
+
!userNameSortAsc ||
|
|
499
|
+
r.length <= 1 ||
|
|
500
|
+
userNameSortAsc.length <= 1
|
|
501
|
+
)
|
|
502
|
+
return true;
|
|
503
|
+
|
|
504
|
+
// Check first user in each list - should be different for different sort directions
|
|
505
|
+
return r[0].id !== userNameSortAsc[0].id;
|
|
506
|
+
},
|
|
507
|
+
// Add JavaScript-based validation of server-side sorting for DESC
|
|
508
|
+
"API userName DESC sort matches JavaScript sort": (r) => {
|
|
509
|
+
if (!r || r.length <= 1) return true;
|
|
510
|
+
|
|
511
|
+
// Create a copy and sort manually by displayName in descending order
|
|
512
|
+
const manualSort = [...r].sort((a, b) => {
|
|
513
|
+
const nameA = a.receiver?.displayName || "";
|
|
514
|
+
const nameB = b.receiver?.displayName || "";
|
|
515
|
+
return nameB.localeCompare(nameA); // Reversed comparison for DESC sort
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Check if the order matches
|
|
519
|
+
return r.every(
|
|
520
|
+
(invitation, index) => invitation.id === manualSort[index].id,
|
|
521
|
+
);
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Test sorting by currentRole
|
|
527
|
+
const roleSortAsc = listCommunityInvitations(communityId, {
|
|
528
|
+
sortBy: InvitationSortField.CURRENT_ROLE,
|
|
529
|
+
sortDirection: SortDirection.ASC,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
console.log(
|
|
533
|
+
"Role sort ASC result:",
|
|
534
|
+
roleSortAsc?.map((inv) => ({
|
|
535
|
+
id: inv.id,
|
|
536
|
+
role: inv.currentRole,
|
|
537
|
+
user: inv.receiver?.displayName,
|
|
538
|
+
})),
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
check(roleSortAsc, {
|
|
542
|
+
"sort by currentRole ASC works": (r) => r !== null && r.length > 0,
|
|
543
|
+
|
|
544
|
+
// Add verification of correct role order for ASC
|
|
545
|
+
"Role ASC sort follows PostgreSQL enum definition order": (r) => {
|
|
546
|
+
if (!r || r.length <= 1) return true;
|
|
547
|
+
|
|
548
|
+
// Define expected order based on enum definition in database
|
|
549
|
+
// In PostgreSQL, ADMIN is defined before MEMBER
|
|
550
|
+
const enumOrder = {
|
|
551
|
+
[MembershipRole.ADMIN]: 1, // First position in enum
|
|
552
|
+
[MembershipRole.MEMBER]: 2, // Second position in enum
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// Create a manual sort using the enum order
|
|
556
|
+
const manualSort = [...r].sort((a, b) => {
|
|
557
|
+
// Use initial_role for entries without current_role (non-accepted invitations)
|
|
558
|
+
const roleA = a.currentRole || a.initialRole;
|
|
559
|
+
const roleB = b.currentRole || b.initialRole;
|
|
560
|
+
|
|
561
|
+
return enumOrder[roleA!] - enumOrder[roleB!];
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// Check if API results match our manual sort
|
|
565
|
+
const orderMatches = r.every(
|
|
566
|
+
(invitation, index) => invitation.id === manualSort[index].id,
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
if (!orderMatches) {
|
|
570
|
+
console.log(
|
|
571
|
+
"Warning: Role ASC order does not match expected enum order",
|
|
572
|
+
);
|
|
573
|
+
console.log(
|
|
574
|
+
"Expected order:",
|
|
575
|
+
manualSort.map((inv) => inv.currentRole || inv.initialRole),
|
|
576
|
+
);
|
|
577
|
+
console.log(
|
|
578
|
+
"Actual order:",
|
|
579
|
+
r.map((inv) => inv.currentRole || inv.initialRole),
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return orderMatches;
|
|
584
|
+
},
|
|
585
|
+
|
|
586
|
+
// Add specific check for ADMIN appearing before MEMBER
|
|
587
|
+
"ADMIN appears before MEMBER in ASC sort": (r) => {
|
|
588
|
+
if (!r || r.length <= 1) return true;
|
|
589
|
+
|
|
590
|
+
const adminIndex = r.findIndex(
|
|
591
|
+
(inv) =>
|
|
592
|
+
(inv.currentRole || inv.initialRole) === MembershipRole.ADMIN,
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
const memberIndex = r.findIndex(
|
|
596
|
+
(inv) =>
|
|
597
|
+
(inv.currentRole || inv.initialRole) === MembershipRole.MEMBER,
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
// If both roles exist, ADMIN should appear first in ASC order
|
|
601
|
+
// since that's the PostgreSQL enum definition order
|
|
602
|
+
if (adminIndex !== -1 && memberIndex !== -1) {
|
|
603
|
+
return adminIndex < memberIndex;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// If only one role exists or none, test passes
|
|
607
|
+
return true;
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Test with DESC
|
|
612
|
+
const roleSortDesc = listCommunityInvitations(communityId, {
|
|
613
|
+
sortBy: InvitationSortField.CURRENT_ROLE,
|
|
614
|
+
sortDirection: SortDirection.DESC,
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
console.log(
|
|
618
|
+
"Role sort DESC result:",
|
|
619
|
+
roleSortDesc?.map((inv) => ({
|
|
620
|
+
id: inv.id,
|
|
621
|
+
role: inv.currentRole,
|
|
622
|
+
user: inv.receiver?.displayName,
|
|
623
|
+
})),
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
check(roleSortDesc, {
|
|
627
|
+
"sort by currentRole DESC works": (r) => r !== null && r.length > 0,
|
|
628
|
+
|
|
629
|
+
// Add verification of correct role order for DESC (reversed)
|
|
630
|
+
"Role DESC sort follows reverse PostgreSQL enum definition order": (
|
|
631
|
+
r,
|
|
632
|
+
) => {
|
|
633
|
+
if (!r || r.length <= 1) return true;
|
|
634
|
+
|
|
635
|
+
// Define expected order based on enum definition in database
|
|
636
|
+
const enumOrder = {
|
|
637
|
+
[MembershipRole.ADMIN]: 1, // First position in enum
|
|
638
|
+
[MembershipRole.MEMBER]: 2, // Second position in enum
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// Create a manual sort using the reversed enum order for DESC
|
|
642
|
+
const manualSort = [...r].sort((a, b) => {
|
|
643
|
+
// Use initial_role for entries without current_role
|
|
644
|
+
const roleA = a.currentRole || a.initialRole;
|
|
645
|
+
const roleB = b.currentRole || b.initialRole;
|
|
646
|
+
|
|
647
|
+
return enumOrder[roleB!] - enumOrder[roleA!]; // Notice the reversed order for DESC
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// Check if API results match our manual sort
|
|
651
|
+
const orderMatches = r.every(
|
|
652
|
+
(invitation, index) => invitation.id === manualSort[index].id,
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
if (!orderMatches) {
|
|
656
|
+
console.log(
|
|
657
|
+
"Warning: Role DESC order does not match expected enum order",
|
|
658
|
+
);
|
|
659
|
+
console.log(
|
|
660
|
+
"Expected order:",
|
|
661
|
+
manualSort.map((inv) => inv.currentRole || inv.initialRole),
|
|
662
|
+
);
|
|
663
|
+
console.log(
|
|
664
|
+
"Actual order:",
|
|
665
|
+
r.map((inv) => inv.currentRole || inv.initialRole),
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return orderMatches;
|
|
670
|
+
},
|
|
671
|
+
|
|
672
|
+
// Add specific check for MEMBER appearing before ADMIN in DESC
|
|
673
|
+
"MEMBER appears before ADMIN in DESC sort": (r) => {
|
|
674
|
+
if (!r || r.length <= 1) return true;
|
|
675
|
+
|
|
676
|
+
const adminIndex = r.findIndex(
|
|
677
|
+
(inv) =>
|
|
678
|
+
(inv.currentRole || inv.initialRole) === MembershipRole.ADMIN,
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
const memberIndex = r.findIndex(
|
|
682
|
+
(inv) =>
|
|
683
|
+
(inv.currentRole || inv.initialRole) === MembershipRole.MEMBER,
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
// If both roles exist, MEMBER should come first in DESC order
|
|
687
|
+
if (adminIndex !== -1 && memberIndex !== -1) {
|
|
688
|
+
return memberIndex < adminIndex;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// If only one role exists or none, test passes
|
|
692
|
+
return true;
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// Add this section before testing lastVisit sorting
|
|
697
|
+
|
|
698
|
+
// Set up lastVisit data for accepted invitations
|
|
699
|
+
describe("Add lastVisit data for sorting tests", () => {
|
|
700
|
+
// Get all accepted invitations
|
|
701
|
+
const allInvitations = listCommunityInvitations(communityId);
|
|
702
|
+
console.log(`Found ${allInvitations?.length || 0} total invitations`);
|
|
703
|
+
|
|
704
|
+
// Find the accepted invitation for our test user
|
|
705
|
+
const acceptedInvitation = allInvitations?.find(
|
|
706
|
+
(inv) => inv.status === InvitationStatus.ACCEPTED,
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
if (acceptedInvitation) {
|
|
710
|
+
console.log(
|
|
711
|
+
`Found accepted invitation for user ${acceptedInvitation.receiver?.displayName}`,
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
// Login as this user to update lastVisit
|
|
715
|
+
logout();
|
|
716
|
+
authenticateWeb(memberUser.login, "password");
|
|
717
|
+
|
|
718
|
+
// Create a timestamp to ensure we have data for sorting
|
|
719
|
+
console.log(`Updating lastVisit data for user ${memberUser.login}`);
|
|
720
|
+
|
|
721
|
+
// Visit different sections with different timestamps
|
|
722
|
+
// This will update lastVisit in the membership entity
|
|
723
|
+
const result = updateLastVisit(
|
|
724
|
+
communityId,
|
|
725
|
+
CommunitySection.ANNOUNCEMENTS,
|
|
726
|
+
);
|
|
727
|
+
check(result, {
|
|
728
|
+
"updateLastVisit successfully sets lastVisit date": (r) =>
|
|
729
|
+
r !== null,
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
// Log back in as admin to proceed with tests
|
|
733
|
+
logout();
|
|
734
|
+
authenticateWeb(adminUser.login, "password");
|
|
735
|
+
} else {
|
|
736
|
+
console.log("No accepted invitations found to update lastVisit");
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Test sorting by lastVisit
|
|
740
|
+
const lastVisitSortAsc = listCommunityInvitations(communityId, {
|
|
741
|
+
sortBy: InvitationSortField.LAST_VISIT,
|
|
742
|
+
sortDirection: SortDirection.ASC,
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
console.log(
|
|
746
|
+
"LastVisit ASC sorting results:",
|
|
747
|
+
lastVisitSortAsc?.map((inv) => ({
|
|
748
|
+
id: inv.id,
|
|
749
|
+
status: inv.status,
|
|
750
|
+
user: inv.receiver?.displayName,
|
|
751
|
+
lastVisit: inv.lastVisit || "null",
|
|
752
|
+
})),
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
check(lastVisitSortAsc, {
|
|
756
|
+
"sort by lastVisit ASC works": (r) => r !== null && r.length > 0,
|
|
757
|
+
"lastVisit ASC sort handles null values correctly": (r) => {
|
|
758
|
+
if (!r || r.length <= 1) return true;
|
|
759
|
+
|
|
760
|
+
// Create a manual sort using JavaScript - PostgreSQL uses NULLS LAST behavior
|
|
761
|
+
const manualSort = [...r].sort((a, b) => {
|
|
762
|
+
// If both have lastVisit, compare dates
|
|
763
|
+
if (a.lastVisit && b.lastVisit) {
|
|
764
|
+
return (
|
|
765
|
+
new Date(a.lastVisit).getTime() -
|
|
766
|
+
new Date(b.lastVisit).getTime()
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
// Handle null cases (NULLS LAST behavior)
|
|
770
|
+
if (!a.lastVisit && b.lastVisit) return 1; // a (null) goes after b
|
|
771
|
+
if (a.lastVisit && !b.lastVisit) return -1; // a goes before b (null)
|
|
772
|
+
return 0; // Both null, keep original order
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// Check if the order matches by comparing IDs at each position
|
|
776
|
+
const orderMatches = r.every(
|
|
777
|
+
(invitation, index) => invitation.id === manualSort[index].id,
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
if (!orderMatches) {
|
|
781
|
+
console.log(
|
|
782
|
+
"Warning: API lastVisit ASC sort order does not match JavaScript sort",
|
|
783
|
+
);
|
|
784
|
+
console.log(
|
|
785
|
+
"Expected order:",
|
|
786
|
+
manualSort.map((inv) => ({
|
|
787
|
+
id: inv.id,
|
|
788
|
+
lastVisit: inv.lastVisit,
|
|
789
|
+
})),
|
|
790
|
+
);
|
|
791
|
+
console.log(
|
|
792
|
+
"Actual order:",
|
|
793
|
+
r.map((inv) => ({ id: inv.id, lastVisit: inv.lastVisit })),
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return orderMatches;
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// Test with DESC
|
|
802
|
+
const lastVisitSortDesc = listCommunityInvitations(communityId, {
|
|
803
|
+
sortBy: InvitationSortField.LAST_VISIT,
|
|
804
|
+
sortDirection: SortDirection.DESC,
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
check(lastVisitSortDesc, {
|
|
808
|
+
"sort by lastVisit DESC works": (r) => r !== null && r.length > 0,
|
|
809
|
+
"lastVisit DESC reverses non-null values but maintains null placement":
|
|
810
|
+
(r) => {
|
|
811
|
+
if (!r || !lastVisitSortAsc || r.length <= 1) return true;
|
|
812
|
+
|
|
813
|
+
// Get entries with lastVisit from both sorts
|
|
814
|
+
const withLastVisitAsc = lastVisitSortAsc.filter(
|
|
815
|
+
(inv) => inv.lastVisit,
|
|
816
|
+
);
|
|
817
|
+
const withLastVisitDesc = r.filter((inv) => inv.lastVisit);
|
|
818
|
+
|
|
819
|
+
// If we don't have enough entries with lastVisit, test passes
|
|
820
|
+
if (withLastVisitAsc.length <= 1 || withLastVisitDesc.length <= 1)
|
|
821
|
+
return true;
|
|
822
|
+
|
|
823
|
+
// In DESC, the non-null values should be in reversed order compared to ASC
|
|
824
|
+
// Use JavaScript's sorting implementation to verify
|
|
825
|
+
const manualSort = [...withLastVisitAsc].sort((a, b) => {
|
|
826
|
+
return (
|
|
827
|
+
new Date(b.lastVisit!).getTime() -
|
|
828
|
+
new Date(a.lastVisit!).getTime()
|
|
829
|
+
);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
// Check if the DESC API results match our manually reversed sort
|
|
833
|
+
const orderMatches = withLastVisitDesc.every(
|
|
834
|
+
(invitation, index) => invitation.id === manualSort[index].id,
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
if (!orderMatches) {
|
|
838
|
+
console.log(
|
|
839
|
+
"Warning: API lastVisit DESC sort does not reverse ASC correctly",
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return orderMatches;
|
|
844
|
+
},
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
// Cleanup - Delete the community
|
|
849
|
+
describe("Cleanup - Delete test community", () => {
|
|
850
|
+
const deleted = deleteCommunity(communityId);
|
|
851
|
+
|
|
852
|
+
check(deleted, {
|
|
853
|
+
"community deleted successfully": (r) => r === true,
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
}
|