@jskit-ai/workspaces-core 0.2.4 → 0.2.6
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/locales/en.json +2 -2
- package/package.json +38 -11
- package/patterns/workspace-server/PATTERN.md +42 -17
- package/patterns/workspace-server/example/packages/main/src/server/WorkspaceInvitationsFeature.js +27 -0
- package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteEmail.js +11 -10
- package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteMailer.js +27 -0
- package/src/server/WorkspacesFeature.js +74 -183
- package/src/server/WorkspacesIntegrationsProvider.js +26 -10
- package/src/server/common/contributors/workspaceActionContextContributor.js +60 -70
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +12 -27
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +48 -63
- package/src/server/common/formatters/workspaceFormatter.js +12 -10
- package/src/server/common/repositories/workspaceInvitesRepository.js +74 -75
- package/src/server/common/repositories/workspaceMembershipsRepository.js +23 -45
- package/src/server/common/repositories/workspacesRepository.js +17 -25
- package/src/server/common/services/{workspaceContextService.js → workspaceDirectoryService.js} +52 -72
- package/src/server/common/support/realtimeServiceEvents.js +20 -12
- package/src/server/common/support/workspaceRecordId.js +21 -0
- package/src/server/common/validators/routeParamsValidator.js +3 -2
- package/src/server/i18n.js +5 -2
- package/src/server/support/resolveWorkspace.js +10 -7
- package/src/server/support/workspaceActionSurfaces.js +5 -41
- package/src/server/support/workspaceCrud.js +25 -0
- package/src/server/support/workspaceInvitationsPolicy.js +16 -27
- package/src/server/support/workspaceRouteInput.js +19 -2
- package/src/server/{common/support → support}/workspaceRoutePaths.js +8 -2
- package/src/server/types.js +47 -0
- package/src/server/workspaceBootstrapContributor.js +105 -142
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +105 -137
- package/src/server/workspaceDirectory/{bootWorkspaceDirectoryRoutes.js → workspaceDirectoryRoutes.js} +7 -19
- package/src/server/workspaceMembers/defaultWorkspaceInviteEmail.js +15 -29
- package/src/server/workspaceMembers/unconfiguredWorkspaceInviteMailer.js +25 -0
- package/src/server/workspaceMembers/workspaceInviteUrls.js +24 -23
- package/src/server/workspaceMembers/workspaceMembersActions.js +189 -219
- package/src/server/workspaceMembers/{bootWorkspaceMembers.js → workspaceMembersRoutes.js} +14 -26
- package/src/server/workspaceMembers/workspaceMembersService.js +110 -97
- package/src/server/workspacePendingInvitations/noInviteAcceptanceParticipant.js +8 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +79 -112
- package/src/server/workspacePendingInvitations/{bootWorkspacePendingInvitations.js → workspacePendingInvitationsRoutes.js} +11 -8
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +54 -57
- package/src/server/workspaceSettings/workspaceSettingsActions.js +53 -73
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +20 -20
- package/src/server/workspaceSettings/{bootWorkspaceSettings.js → workspaceSettingsRoutes.js} +9 -21
- package/src/server/workspaceSettings/workspaceSettingsService.js +24 -26
- package/src/server/workspacesRuntime.js +185 -0
- package/src/shared/operationMessages.js +6 -15
- package/src/shared/resources/workspaceMembersResource.js +2 -2
- package/src/shared/resources/workspacePendingInvitationsResource.js +2 -2
- package/src/shared/resources/workspaceResource.js +4 -1
- package/src/shared/roles.js +15 -12
- package/src/shared/tenancyProfile.js +2 -2
- package/test/exportsContract.test.js +5 -0
- package/test/featureRuntime.test.js +5 -6
- package/test/packageMetadata.test.js +12 -0
- package/test/repositories.integration.test.js +1 -1
- package/test/requestedWorkspaceScope.test.js +24 -0
- package/test/roleLabels.test.js +29 -7
- package/test/support/invitationTransactions.js +35 -19
- package/test/workspaceActionSurfaces.test.js +0 -55
- package/test/workspaceAuthPolicyContextResolver.test.js +1 -1
- package/test/workspaceBootstrapContributor.test.js +7 -6
- package/test/workspaceCrud.test.js +23 -0
- package/test/workspaceInvitesRepository.test.js +9 -8
- package/test/workspaceMembersService.test.js +131 -28
- package/test/workspacePendingInvitationsResource.test.js +2 -2
- package/test/workspacePendingInvitationsService.test.js +69 -22
- package/test/workspaceService.test.js +58 -47
- package/test/workspaceSettingsActions.test.js +14 -7
- package/test/workspaceSettingsRepository.test.js +11 -0
- package/test/workspaceSettingsService.test.js +11 -9
- package/test/workspacesRepository.test.js +16 -16
- package/test/workspacesRouteRequestInputValidator.test.js +4 -4
- package/src/server/common/repositories/repositoryUtils.js +0 -43
- package/src/server/common/support/deepFreeze.js +0 -1
- package/src/server/common/support/resolveActionUser.js +0 -9
|
@@ -3,9 +3,13 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
|
+
import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
|
|
6
7
|
import { createWorkspacesServerI18n } from "../src/server/i18n.js";
|
|
7
8
|
import { createService } from "../src/server/workspaceMembers/workspaceMembersService.js";
|
|
8
9
|
import { createWorkspaceRoleCatalog } from "../src/shared/roles.js";
|
|
10
|
+
import { createWorkspaceInviteUrlBuilder } from "../src/server/workspaceMembers/workspaceInviteUrls.js";
|
|
11
|
+
import { renderDefaultWorkspaceInviteEmail } from "../src/server/workspaceMembers/defaultWorkspaceInviteEmail.js";
|
|
12
|
+
import { unconfiguredWorkspaceInviteMailer } from "../src/server/workspaceMembers/unconfiguredWorkspaceInviteMailer.js";
|
|
9
13
|
|
|
10
14
|
function authorizedOptions(permissions = []) {
|
|
11
15
|
return {
|
|
@@ -45,13 +49,15 @@ function createRoleCatalog() {
|
|
|
45
49
|
function createFixture() {
|
|
46
50
|
const workspace = {
|
|
47
51
|
id: "7",
|
|
48
|
-
slug: "
|
|
49
|
-
name: "
|
|
52
|
+
slug: "tony3",
|
|
53
|
+
name: "Tony3",
|
|
50
54
|
ownerUserId: "9",
|
|
51
55
|
avatarUrl: ""
|
|
52
56
|
};
|
|
53
57
|
|
|
54
58
|
const service = createService({
|
|
59
|
+
workspaceInvitationsEnabled: true,
|
|
60
|
+
i18n: createWorkspacesServerI18n(),
|
|
55
61
|
workspaceMembershipsRepository: {
|
|
56
62
|
async listActiveByWorkspaceId(workspaceId) {
|
|
57
63
|
assert.equal(Number(workspaceId), 7);
|
|
@@ -97,7 +103,10 @@ function createFixture() {
|
|
|
97
103
|
async revokeById() {}
|
|
98
104
|
},
|
|
99
105
|
inviteExpiresInMs: 7 * 24 * 60 * 60 * 1000,
|
|
100
|
-
roleCatalog: createRoleCatalog()
|
|
106
|
+
roleCatalog: createRoleCatalog(),
|
|
107
|
+
inviteUrlBuilder: createWorkspaceInviteUrlBuilder({ appConfig: {}, env: {} }),
|
|
108
|
+
workspaceInviteMailer: unconfiguredWorkspaceInviteMailer,
|
|
109
|
+
workspaceInviteEmailTemplate: renderDefaultWorkspaceInviteEmail
|
|
101
110
|
});
|
|
102
111
|
|
|
103
112
|
return { service, workspace };
|
|
@@ -107,6 +116,8 @@ test("workspaceMembersService.createInvite uses configured inviteExpiresInMs", a
|
|
|
107
116
|
const expiresAtValues = [];
|
|
108
117
|
let savedInvite;
|
|
109
118
|
const service = createService({
|
|
119
|
+
workspaceInvitationsEnabled: true,
|
|
120
|
+
i18n: createWorkspacesServerI18n(),
|
|
110
121
|
workspaceMembershipsRepository: {
|
|
111
122
|
async listActiveByWorkspaceId() {
|
|
112
123
|
return [];
|
|
@@ -132,11 +143,14 @@ test("workspaceMembersService.createInvite uses configured inviteExpiresInMs", a
|
|
|
132
143
|
async revokeById() {}
|
|
133
144
|
},
|
|
134
145
|
inviteExpiresInMs: 30 * 60 * 1000,
|
|
135
|
-
roleCatalog: createRoleCatalog()
|
|
146
|
+
roleCatalog: createRoleCatalog(),
|
|
147
|
+
inviteUrlBuilder: createWorkspaceInviteUrlBuilder({ appConfig: {}, env: {} }),
|
|
148
|
+
workspaceInviteMailer: unconfiguredWorkspaceInviteMailer,
|
|
149
|
+
workspaceInviteEmailTemplate: renderDefaultWorkspaceInviteEmail
|
|
136
150
|
});
|
|
137
151
|
|
|
138
152
|
const before = Date.now();
|
|
139
|
-
const response = await service.createInvite(
|
|
153
|
+
const response = (await service.createInvite(
|
|
140
154
|
{
|
|
141
155
|
id: "7",
|
|
142
156
|
ownerUserId: "9"
|
|
@@ -147,7 +161,7 @@ test("workspaceMembersService.createInvite uses configured inviteExpiresInMs", a
|
|
|
147
161
|
roleSid: "member"
|
|
148
162
|
},
|
|
149
163
|
authorizedOptions(["workspace.members.invite"])
|
|
150
|
-
);
|
|
164
|
+
)).value;
|
|
151
165
|
const after = Date.now();
|
|
152
166
|
|
|
153
167
|
assert.equal(expiresAtValues.length, 1);
|
|
@@ -165,11 +179,68 @@ test("workspaceMembersService.createInvite uses configured inviteExpiresInMs", a
|
|
|
165
179
|
});
|
|
166
180
|
});
|
|
167
181
|
|
|
182
|
+
test("workspaceMembersService.createInvite without a provided mailer neither looks up the recipient nor renders", async () => {
|
|
183
|
+
const lookups = [];
|
|
184
|
+
let savedInvite;
|
|
185
|
+
const service = createService({
|
|
186
|
+
workspaceInvitationsEnabled: true,
|
|
187
|
+
i18n: createWorkspacesServerI18n(),
|
|
188
|
+
workspaceMembershipsRepository: {
|
|
189
|
+
async listActiveByWorkspaceId() {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
workspaceInvitesRepository: {
|
|
194
|
+
async withTransaction(work) {
|
|
195
|
+
return work({});
|
|
196
|
+
},
|
|
197
|
+
async lockWorkspaceForInvitations() {},
|
|
198
|
+
async expirePendingByWorkspaceIdAndEmail() {},
|
|
199
|
+
async insert(payload) {
|
|
200
|
+
savedInvite = { ...payload, id: "32" };
|
|
201
|
+
return savedInvite;
|
|
202
|
+
},
|
|
203
|
+
async listPendingByWorkspaceIdWithWorkspace() {
|
|
204
|
+
return [];
|
|
205
|
+
},
|
|
206
|
+
async findPendingByIdForWorkspace() {
|
|
207
|
+
return savedInvite;
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
userProfilesRepository: {
|
|
211
|
+
async findByEmail(email) {
|
|
212
|
+
lookups.push(email);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
inviteExpiresInMs: 30 * 60 * 1000,
|
|
217
|
+
roleCatalog: createRoleCatalog(),
|
|
218
|
+
inviteUrlBuilder: createWorkspaceInviteUrlBuilder({ appConfig: {}, env: {} }),
|
|
219
|
+
workspaceInviteMailer: unconfiguredWorkspaceInviteMailer,
|
|
220
|
+
workspaceInviteEmailTemplate() {
|
|
221
|
+
throw new Error("No email is rendered without a mailer.");
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const response = (await service.createInvite(
|
|
226
|
+
{ id: "7", ownerUserId: "9" },
|
|
227
|
+
{ id: "11" },
|
|
228
|
+
{ email: "alice@example.com", roleSid: "member" },
|
|
229
|
+
authorizedOptions(["workspace.members.invite"])
|
|
230
|
+
)).value;
|
|
231
|
+
|
|
232
|
+
assert.deepEqual(lookups, []);
|
|
233
|
+
assert.equal(response.inviteDelivery.status, "mailer_unconfigured");
|
|
234
|
+
assert.equal(response.inviteDelivery.messageCode, "workspaces.invite.mailerUnconfigured");
|
|
235
|
+
});
|
|
236
|
+
|
|
168
237
|
test("workspaceMembersService.createInvite sends generated invite URLs through the configured mailer", async () => {
|
|
169
238
|
const templateCalls = [];
|
|
170
239
|
const mailerCalls = [];
|
|
171
240
|
let savedInvite;
|
|
172
241
|
const service = createService({
|
|
242
|
+
workspaceInvitationsEnabled: true,
|
|
243
|
+
i18n: createWorkspacesServerI18n(),
|
|
173
244
|
workspaceMembershipsRepository: {
|
|
174
245
|
async listActiveByWorkspaceId() {
|
|
175
246
|
return [];
|
|
@@ -224,7 +295,7 @@ test("workspaceMembersService.createInvite sends generated invite URLs through t
|
|
|
224
295
|
}
|
|
225
296
|
});
|
|
226
297
|
|
|
227
|
-
const response = await service.createInvite(
|
|
298
|
+
const response = (await service.createInvite(
|
|
228
299
|
{
|
|
229
300
|
id: "7",
|
|
230
301
|
slug: "acme",
|
|
@@ -238,7 +309,7 @@ test("workspaceMembersService.createInvite sends generated invite URLs through t
|
|
|
238
309
|
roleSid: "admin"
|
|
239
310
|
},
|
|
240
311
|
authorizedOptions(["workspace.members.invite"])
|
|
241
|
-
);
|
|
312
|
+
)).value;
|
|
242
313
|
|
|
243
314
|
assert.equal(response.createdInviteId, "88");
|
|
244
315
|
assert.match(response.inviteUrl, /^https:\/\/app\.example\.test\/invite\//);
|
|
@@ -262,6 +333,8 @@ test("workspaceMembersService.createInvite sends generated invite URLs through t
|
|
|
262
333
|
test("workspaceMembersService.revokeInvite returns the revoked invite id", async () => {
|
|
263
334
|
let revokedInviteId = 0;
|
|
264
335
|
const service = createService({
|
|
336
|
+
workspaceInvitationsEnabled: true,
|
|
337
|
+
i18n: createWorkspacesServerI18n(),
|
|
265
338
|
workspaceMembershipsRepository: {
|
|
266
339
|
async listActiveByWorkspaceId() {
|
|
267
340
|
return [];
|
|
@@ -294,17 +367,20 @@ test("workspaceMembersService.revokeInvite returns the revoked invite id", async
|
|
|
294
367
|
}
|
|
295
368
|
},
|
|
296
369
|
inviteExpiresInMs: 30 * 60 * 1000,
|
|
297
|
-
roleCatalog: createRoleCatalog()
|
|
370
|
+
roleCatalog: createRoleCatalog(),
|
|
371
|
+
inviteUrlBuilder: createWorkspaceInviteUrlBuilder({ appConfig: {}, env: {} }),
|
|
372
|
+
workspaceInviteMailer: unconfiguredWorkspaceInviteMailer,
|
|
373
|
+
workspaceInviteEmailTemplate: renderDefaultWorkspaceInviteEmail
|
|
298
374
|
});
|
|
299
375
|
|
|
300
|
-
const response = await service.revokeInvite(
|
|
376
|
+
const response = (await service.revokeInvite(
|
|
301
377
|
{
|
|
302
378
|
id: "7",
|
|
303
379
|
ownerUserId: "9"
|
|
304
380
|
},
|
|
305
381
|
"47",
|
|
306
382
|
authorizedOptions(["workspace.invites.revoke"])
|
|
307
|
-
);
|
|
383
|
+
)).value;
|
|
308
384
|
|
|
309
385
|
assert.equal(revokedInviteId, 47);
|
|
310
386
|
assert.equal(response.revokedInviteId, "47");
|
|
@@ -312,6 +388,7 @@ test("workspaceMembersService.revokeInvite returns the revoked invite id", async
|
|
|
312
388
|
|
|
313
389
|
test("workspaceMembersService rejects invite operations when invitations are disabled", async () => {
|
|
314
390
|
const service = createService({
|
|
391
|
+
i18n: createWorkspacesServerI18n(),
|
|
315
392
|
workspaceMembershipsRepository: {
|
|
316
393
|
async listActiveByWorkspaceId() {
|
|
317
394
|
return [];
|
|
@@ -336,12 +413,15 @@ test("workspaceMembersService rejects invite operations when invitations are dis
|
|
|
336
413
|
},
|
|
337
414
|
inviteExpiresInMs: 30 * 60 * 1000,
|
|
338
415
|
roleCatalog: createRoleCatalog(),
|
|
339
|
-
workspaceInvitationsEnabled: false
|
|
416
|
+
workspaceInvitationsEnabled: false,
|
|
417
|
+
inviteUrlBuilder: createWorkspaceInviteUrlBuilder({ appConfig: {}, env: {} }),
|
|
418
|
+
workspaceInviteMailer: unconfiguredWorkspaceInviteMailer,
|
|
419
|
+
workspaceInviteEmailTemplate: renderDefaultWorkspaceInviteEmail
|
|
340
420
|
});
|
|
341
421
|
|
|
342
422
|
await assert.rejects(
|
|
343
423
|
() =>
|
|
344
|
-
service.
|
|
424
|
+
service.listInvitesResponse(
|
|
345
425
|
{
|
|
346
426
|
id: "7",
|
|
347
427
|
ownerUserId: "9"
|
|
@@ -380,6 +460,17 @@ test("workspaceMembersService rejects invite operations when invitations are dis
|
|
|
380
460
|
);
|
|
381
461
|
});
|
|
382
462
|
|
|
463
|
+
test("workspaceMembersService answers the role and member list actions with JSON:API data", async () => {
|
|
464
|
+
const { service, workspace } = createFixture();
|
|
465
|
+
const options = authorizedOptions(["workspace.members.view"]);
|
|
466
|
+
|
|
467
|
+
assert.deepEqual(await service.listRolesResponse(options), returnJsonApiData(await service.listRoles(options)));
|
|
468
|
+
assert.deepEqual(
|
|
469
|
+
await service.listMembersResponse(workspace, options),
|
|
470
|
+
returnJsonApiData(await service.listMembers(workspace, options))
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
383
474
|
test("workspaceMembersService.listMembers uses the resolved workspace directly", async () => {
|
|
384
475
|
const { service, workspace } = createFixture();
|
|
385
476
|
|
|
@@ -387,8 +478,8 @@ test("workspaceMembersService.listMembers uses the resolved workspace directly",
|
|
|
387
478
|
|
|
388
479
|
assert.deepEqual(response.workspace, {
|
|
389
480
|
id: "7",
|
|
390
|
-
slug: "
|
|
391
|
-
name: "
|
|
481
|
+
slug: "tony3",
|
|
482
|
+
name: "Tony3",
|
|
392
483
|
ownerUserId: "9",
|
|
393
484
|
avatarUrl: ""
|
|
394
485
|
});
|
|
@@ -399,14 +490,14 @@ test("workspaceMembersService.listMembers uses the resolved workspace directly",
|
|
|
399
490
|
test("workspaceMembersService.updateMemberRole returns the refreshed member list without re-fetching the workspace", async () => {
|
|
400
491
|
const { service, workspace } = createFixture();
|
|
401
492
|
|
|
402
|
-
const response = await service.updateMemberRole(
|
|
493
|
+
const response = (await service.updateMemberRole(
|
|
403
494
|
workspace,
|
|
404
495
|
{
|
|
405
496
|
memberUserId: "11",
|
|
406
497
|
roleSid: "admin"
|
|
407
498
|
},
|
|
408
499
|
authorizedOptions(["workspace.members.manage"])
|
|
409
|
-
);
|
|
500
|
+
)).value;
|
|
410
501
|
|
|
411
502
|
assert.equal(response.members.length, 1);
|
|
412
503
|
assert.equal(response.members[0].roleSid, "member");
|
|
@@ -416,12 +507,14 @@ test("workspaceMembersService.removeMember marks membership revoked and returns
|
|
|
416
507
|
let removed = false;
|
|
417
508
|
const workspace = {
|
|
418
509
|
id: "7",
|
|
419
|
-
slug: "
|
|
420
|
-
name: "
|
|
510
|
+
slug: "tony3",
|
|
511
|
+
name: "Tony3",
|
|
421
512
|
ownerUserId: "9",
|
|
422
513
|
avatarUrl: ""
|
|
423
514
|
};
|
|
424
515
|
const service = createService({
|
|
516
|
+
workspaceInvitationsEnabled: true,
|
|
517
|
+
i18n: createWorkspacesServerI18n(),
|
|
425
518
|
workspaceMembershipsRepository: {
|
|
426
519
|
async listActiveByWorkspaceId(workspaceId) {
|
|
427
520
|
assert.equal(Number(workspaceId), 7);
|
|
@@ -469,16 +562,19 @@ test("workspaceMembersService.removeMember marks membership revoked and returns
|
|
|
469
562
|
async revokeById() {}
|
|
470
563
|
},
|
|
471
564
|
inviteExpiresInMs: 7 * 24 * 60 * 60 * 1000,
|
|
472
|
-
roleCatalog: createRoleCatalog()
|
|
565
|
+
roleCatalog: createRoleCatalog(),
|
|
566
|
+
inviteUrlBuilder: createWorkspaceInviteUrlBuilder({ appConfig: {}, env: {} }),
|
|
567
|
+
workspaceInviteMailer: unconfiguredWorkspaceInviteMailer,
|
|
568
|
+
workspaceInviteEmailTemplate: renderDefaultWorkspaceInviteEmail
|
|
473
569
|
});
|
|
474
570
|
|
|
475
|
-
const response = await service.removeMember(
|
|
571
|
+
const response = (await service.removeMember(
|
|
476
572
|
workspace,
|
|
477
573
|
{
|
|
478
574
|
memberUserId: "11"
|
|
479
575
|
},
|
|
480
576
|
authorizedOptions(["workspace.members.manage"])
|
|
481
|
-
);
|
|
577
|
+
)).value;
|
|
482
578
|
|
|
483
579
|
assert.equal(response.members.length, 0);
|
|
484
580
|
});
|
|
@@ -486,12 +582,14 @@ test("workspaceMembersService.removeMember marks membership revoked and returns
|
|
|
486
582
|
test("workspaceMembersService.removeMember rejects removing the owner", async () => {
|
|
487
583
|
const workspace = {
|
|
488
584
|
id: "7",
|
|
489
|
-
slug: "
|
|
490
|
-
name: "
|
|
585
|
+
slug: "tony3",
|
|
586
|
+
name: "Tony3",
|
|
491
587
|
ownerUserId: "9",
|
|
492
588
|
avatarUrl: ""
|
|
493
589
|
};
|
|
494
590
|
const service = createService({
|
|
591
|
+
workspaceInvitationsEnabled: true,
|
|
592
|
+
i18n: createWorkspacesServerI18n(),
|
|
495
593
|
workspaceMembershipsRepository: {
|
|
496
594
|
async listActiveByWorkspaceId() {
|
|
497
595
|
return [];
|
|
@@ -522,7 +620,10 @@ test("workspaceMembersService.removeMember rejects removing the owner", async ()
|
|
|
522
620
|
async revokeById() {}
|
|
523
621
|
},
|
|
524
622
|
inviteExpiresInMs: 7 * 24 * 60 * 60 * 1000,
|
|
525
|
-
roleCatalog: createRoleCatalog()
|
|
623
|
+
roleCatalog: createRoleCatalog(),
|
|
624
|
+
inviteUrlBuilder: createWorkspaceInviteUrlBuilder({ appConfig: {}, env: {} }),
|
|
625
|
+
workspaceInviteMailer: unconfiguredWorkspaceInviteMailer,
|
|
626
|
+
workspaceInviteEmailTemplate: renderDefaultWorkspaceInviteEmail
|
|
526
627
|
});
|
|
527
628
|
|
|
528
629
|
await assert.rejects(
|
|
@@ -541,6 +642,7 @@ test("workspaceMembersService.removeMember rejects removing the owner", async ()
|
|
|
541
642
|
function createMailingService({ accounts = {}, appRoot = "", mailer } = {}) {
|
|
542
643
|
let savedInvite;
|
|
543
644
|
return createService({
|
|
645
|
+
workspaceInvitationsEnabled: true,
|
|
544
646
|
workspaceMembershipsRepository: {
|
|
545
647
|
async listActiveByWorkspaceId() {
|
|
546
648
|
return [];
|
|
@@ -581,7 +683,8 @@ function createMailingService({ accounts = {}, appRoot = "", mailer } = {}) {
|
|
|
581
683
|
}
|
|
582
684
|
return account ? { locale: account.locale } : null;
|
|
583
685
|
}
|
|
584
|
-
}
|
|
686
|
+
},
|
|
687
|
+
workspaceInviteEmailTemplate: renderDefaultWorkspaceInviteEmail
|
|
585
688
|
});
|
|
586
689
|
}
|
|
587
690
|
|
|
@@ -644,12 +747,12 @@ test("workspaceMembersService reports a failed delivery with a message code", as
|
|
|
644
747
|
}
|
|
645
748
|
}
|
|
646
749
|
});
|
|
647
|
-
const response = await service.createInvite(
|
|
750
|
+
const response = (await service.createInvite(
|
|
648
751
|
{ id: "7", slug: "acme", name: "Acme", ownerUserId: "9", avatarUrl: "" },
|
|
649
752
|
{ id: "9" },
|
|
650
753
|
{ email: "invitee@example.com", roleSid: "member" },
|
|
651
754
|
authorizedOptions()
|
|
652
|
-
);
|
|
755
|
+
)).value;
|
|
653
756
|
assert.deepEqual(response.inviteDelivery, {
|
|
654
757
|
status: "failed",
|
|
655
758
|
message: "Unable to send workspace invite email.",
|
|
@@ -13,8 +13,8 @@ test("workspacePendingInvitationsResource output schema accepts already-shaped i
|
|
|
13
13
|
{
|
|
14
14
|
id: "10",
|
|
15
15
|
workspaceId: "3",
|
|
16
|
-
workspaceSlug: "
|
|
17
|
-
workspaceName: "
|
|
16
|
+
workspaceSlug: "tony3",
|
|
17
|
+
workspaceName: "Tony3",
|
|
18
18
|
workspaceAvatarUrl: "",
|
|
19
19
|
roleSid: "member",
|
|
20
20
|
status: "pending",
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { encodeInviteTokenHash } from "@jskit-ai/auth-core/shared/inviteTokens";
|
|
4
|
+
import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
|
|
4
5
|
import { createService } from "../src/server/workspacePendingInvitations/workspacePendingInvitationsService.js";
|
|
6
|
+
import { noInviteAcceptanceParticipant } from "../src/server/workspacePendingInvitations/noInviteAcceptanceParticipant.js";
|
|
7
|
+
import { createWorkspaceRoleCatalog } from "../src/shared/roles.js";
|
|
5
8
|
|
|
6
9
|
function createFixture({
|
|
7
10
|
pendingInvitesByEmail = [],
|
|
@@ -53,7 +56,9 @@ function createFixture({
|
|
|
53
56
|
payload: payload && typeof payload === "object" ? { ...payload } : payload
|
|
54
57
|
});
|
|
55
58
|
}
|
|
56
|
-
}
|
|
59
|
+
},
|
|
60
|
+
roleCatalog: createWorkspaceRoleCatalog(),
|
|
61
|
+
acceptanceParticipant: noInviteAcceptanceParticipant
|
|
57
62
|
});
|
|
58
63
|
|
|
59
64
|
return {
|
|
@@ -68,15 +73,15 @@ function createFixture({
|
|
|
68
73
|
};
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
test("listPendingInvitesForUser returns raw pending invite rows
|
|
76
|
+
test("listPendingInvitesForUser returns raw pending invite rows", async () => {
|
|
72
77
|
const tokenHash = "a".repeat(64);
|
|
73
78
|
const { service } = createFixture({
|
|
74
79
|
pendingInvitesByEmail: [
|
|
75
80
|
{
|
|
76
81
|
id: "10",
|
|
77
82
|
workspaceId: "1",
|
|
78
|
-
workspaceSlug: "
|
|
79
|
-
workspaceName: "
|
|
83
|
+
workspaceSlug: "tony3",
|
|
84
|
+
workspaceName: "Tony3",
|
|
80
85
|
workspaceAvatarUrl: "",
|
|
81
86
|
roleSid: "member",
|
|
82
87
|
status: "pending",
|
|
@@ -88,15 +93,39 @@ test("listPendingInvitesForUser returns raw pending invite rows for the action l
|
|
|
88
93
|
|
|
89
94
|
const pendingInvites = await service.listPendingInvitesForUser({
|
|
90
95
|
id: "7",
|
|
91
|
-
email: "
|
|
96
|
+
email: "chiara@example.com"
|
|
92
97
|
});
|
|
93
98
|
|
|
94
99
|
assert.equal(pendingInvites.length, 1);
|
|
95
100
|
assert.equal(pendingInvites[0].token, encodeInviteTokenHash(tokenHash));
|
|
96
|
-
assert.equal(pendingInvites[0].workspaceName, "
|
|
101
|
+
assert.equal(pendingInvites[0].workspaceName, "Tony3");
|
|
97
102
|
assert.equal(pendingInvites[0].status, "pending");
|
|
98
103
|
});
|
|
99
104
|
|
|
105
|
+
test("listPendingInvitesResponse answers the pending list action with the rows as JSON:API data", async () => {
|
|
106
|
+
const { service } = createFixture({
|
|
107
|
+
pendingInvitesByEmail: [
|
|
108
|
+
{
|
|
109
|
+
id: "10",
|
|
110
|
+
workspaceId: "1",
|
|
111
|
+
workspaceSlug: "tony3",
|
|
112
|
+
workspaceName: "Tony3",
|
|
113
|
+
workspaceAvatarUrl: "",
|
|
114
|
+
roleSid: "member",
|
|
115
|
+
status: "pending",
|
|
116
|
+
expiresAt: "2030-01-01T00:00:00.000Z",
|
|
117
|
+
tokenHash: "b".repeat(64)
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
});
|
|
121
|
+
const user = { id: "7", email: "chiara@example.com" };
|
|
122
|
+
|
|
123
|
+
assert.deepEqual(
|
|
124
|
+
await service.listPendingInvitesResponse(user),
|
|
125
|
+
returnJsonApiData({ pendingInvites: await service.listPendingInvitesForUser(user) })
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
100
129
|
test("resolveInviteByToken returns safe public invite metadata without mutating the invite", async () => {
|
|
101
130
|
const tokenHash = "d".repeat(64);
|
|
102
131
|
const encodedToken = encodeInviteTokenHash(tokenHash);
|
|
@@ -117,7 +146,7 @@ test("resolveInviteByToken returns safe public invite metadata without mutating
|
|
|
117
146
|
}
|
|
118
147
|
});
|
|
119
148
|
|
|
120
|
-
const resolved = await service.resolveInviteByToken(encodedToken);
|
|
149
|
+
const resolved = (await service.resolveInviteByToken(encodedToken)).value;
|
|
121
150
|
|
|
122
151
|
assert.deepEqual(calls.resolveTokenHashCalls, [tokenHash]);
|
|
123
152
|
assert.deepEqual(calls.revokeCalls, []);
|
|
@@ -157,8 +186,8 @@ test("resolveInviteByToken reports expired and missing invitations as terminal s
|
|
|
157
186
|
}
|
|
158
187
|
});
|
|
159
188
|
|
|
160
|
-
const expired = await service.resolveInviteByToken(expiredToken);
|
|
161
|
-
const missing = await service.resolveInviteByToken(missingToken);
|
|
189
|
+
const expired = (await service.resolveInviteByToken(expiredToken)).value;
|
|
190
|
+
const missing = (await service.resolveInviteByToken(missingToken)).value;
|
|
162
191
|
|
|
163
192
|
assert.equal(expired.status, "expired");
|
|
164
193
|
assert.equal(missing.status, "not_found");
|
|
@@ -219,7 +248,7 @@ test("acceptInviteByToken accepts opaque invite token and resolves invite by dec
|
|
|
219
248
|
[tokenHash]: {
|
|
220
249
|
id: "44",
|
|
221
250
|
workspaceId: "1",
|
|
222
|
-
email: "
|
|
251
|
+
email: "chiara@example.com",
|
|
223
252
|
roleSid: "member",
|
|
224
253
|
status: "pending",
|
|
225
254
|
tokenHash,
|
|
@@ -231,7 +260,7 @@ test("acceptInviteByToken accepts opaque invite token and resolves invite by dec
|
|
|
231
260
|
const response = await service.acceptInviteByToken({
|
|
232
261
|
user: {
|
|
233
262
|
id: "7",
|
|
234
|
-
email: "
|
|
263
|
+
email: "chiara@example.com",
|
|
235
264
|
displayName: "Chiara"
|
|
236
265
|
},
|
|
237
266
|
token: encodedToken
|
|
@@ -253,7 +282,7 @@ test("refuseInviteByToken revokes the invite and returns refused", async () => {
|
|
|
253
282
|
[tokenHash]: {
|
|
254
283
|
id: "45",
|
|
255
284
|
workspaceId: "1",
|
|
256
|
-
email: "
|
|
285
|
+
email: "chiara@example.com",
|
|
257
286
|
roleSid: "member",
|
|
258
287
|
status: "pending",
|
|
259
288
|
tokenHash,
|
|
@@ -265,7 +294,7 @@ test("refuseInviteByToken revokes the invite and returns refused", async () => {
|
|
|
265
294
|
const response = await service.refuseInviteByToken({
|
|
266
295
|
user: {
|
|
267
296
|
id: "7",
|
|
268
|
-
email: "
|
|
297
|
+
email: "chiara@example.com",
|
|
269
298
|
displayName: "Chiara"
|
|
270
299
|
},
|
|
271
300
|
token: encodedToken
|
|
@@ -279,14 +308,30 @@ test("refuseInviteByToken revokes the invite and returns refused", async () => {
|
|
|
279
308
|
assert.equal(response.workspaceId, "1");
|
|
280
309
|
});
|
|
281
310
|
|
|
282
|
-
test("
|
|
283
|
-
const {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
311
|
+
test("redeemInviteByToken accepts on an accept decision and refuses on any other", async () => {
|
|
312
|
+
const user = { id: "7", email: "chiara@example.com", displayName: "Chiara" };
|
|
313
|
+
for (const [decision, expected] of [["accept", "accepted"], ["refuse", "refused"]]) {
|
|
314
|
+
const tokenHash = "d".repeat(64);
|
|
315
|
+
const { service, calls } = createFixture({
|
|
316
|
+
inviteByTokenHash: {
|
|
317
|
+
[tokenHash]: {
|
|
318
|
+
id: "46",
|
|
319
|
+
workspaceId: "1",
|
|
320
|
+
email: user.email,
|
|
321
|
+
roleSid: "member",
|
|
322
|
+
status: "pending",
|
|
323
|
+
tokenHash,
|
|
324
|
+
expiresAt: "2030-01-01T00:00:00.000Z"
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const response = (await service.redeemInviteByToken(user, { token: encodeInviteTokenHash(tokenHash), decision })).value;
|
|
330
|
+
|
|
331
|
+
assert.equal(response.decision, expected);
|
|
332
|
+
assert.deepEqual(calls.acceptCalls, decision === "accept" ? [46] : []);
|
|
333
|
+
assert.deepEqual(calls.revokeCalls, decision === "accept" ? [] : [46]);
|
|
334
|
+
}
|
|
290
335
|
});
|
|
291
336
|
|
|
292
337
|
for (const transactionOutcome of ["committed", "unknown", "rolledBack"]) {
|
|
@@ -302,7 +347,9 @@ for (const transactionOutcome of ["committed", "unknown", "rolledBack"]) {
|
|
|
302
347
|
throw failure;
|
|
303
348
|
}
|
|
304
349
|
},
|
|
305
|
-
workspaceMembershipsRepository: {}
|
|
350
|
+
workspaceMembershipsRepository: {},
|
|
351
|
+
roleCatalog: createWorkspaceRoleCatalog(),
|
|
352
|
+
acceptanceParticipant: noInviteAcceptanceParticipant
|
|
306
353
|
});
|
|
307
354
|
await assert.rejects(service.acceptInviteByToken({ user: { id: "1", email: "a@example.test" }, token: "valid-token" }),
|
|
308
355
|
(error) => error === failure && error.cause === cause);
|