@jskit-ai/workspaces-core 0.1.196 → 0.1.198
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/package.json +9 -9
- package/patterns/workspace-server/PATTERN.md +49 -0
- package/src/server/common/repositories/workspaceInvitesRepository.js +89 -44
- package/src/server/workspaceMembers/workspaceMembersService.js +67 -40
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +1 -1
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +66 -42
- package/test/featureRuntime.test.js +5 -0
- package/test/repositories.integration.test.js +18 -2
- package/test/support/invitationTransactions.js +476 -0
- package/test/workspaceInvitesRepository.test.js +10 -2
- package/test/workspaceMembersService.test.js +22 -9
- package/test/workspacePendingInvitationsService.test.js +30 -2
|
@@ -81,6 +81,8 @@ function createFixture() {
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
workspaceInvitesRepository: {
|
|
84
|
+
async withTransaction(work) { return work({}); },
|
|
85
|
+
async lockWorkspaceForInvitations() {},
|
|
84
86
|
async listPendingByWorkspaceIdWithWorkspace(workspaceId) {
|
|
85
87
|
assert.equal(Number(workspaceId), 7);
|
|
86
88
|
return [];
|
|
@@ -101,6 +103,7 @@ function createFixture() {
|
|
|
101
103
|
|
|
102
104
|
test("workspaceMembersService.createInvite uses configured inviteExpiresInMs", async () => {
|
|
103
105
|
const expiresAtValues = [];
|
|
106
|
+
let savedInvite;
|
|
104
107
|
const service = createService({
|
|
105
108
|
workspaceMembershipsRepository: {
|
|
106
109
|
async listActiveByWorkspaceId() {
|
|
@@ -108,18 +111,19 @@ test("workspaceMembersService.createInvite uses configured inviteExpiresInMs", a
|
|
|
108
111
|
}
|
|
109
112
|
},
|
|
110
113
|
workspaceInvitesRepository: {
|
|
114
|
+
async withTransaction(work) { return work({}); },
|
|
115
|
+
async lockWorkspaceForInvitations() {},
|
|
111
116
|
async expirePendingByWorkspaceIdAndEmail() {},
|
|
112
117
|
async insert(payload) {
|
|
113
118
|
expiresAtValues.push(payload.expiresAt);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
};
|
|
119
|
+
savedInvite = { ...payload, id: "31" };
|
|
120
|
+
return savedInvite;
|
|
117
121
|
},
|
|
118
122
|
async listPendingByWorkspaceIdWithWorkspace() {
|
|
119
123
|
return [];
|
|
120
124
|
},
|
|
121
125
|
async findPendingByIdForWorkspace() {
|
|
122
|
-
return
|
|
126
|
+
return savedInvite;
|
|
123
127
|
},
|
|
124
128
|
async revokeById() {}
|
|
125
129
|
},
|
|
@@ -158,6 +162,7 @@ test("workspaceMembersService.createInvite uses configured inviteExpiresInMs", a
|
|
|
158
162
|
test("workspaceMembersService.createInvite sends generated invite URLs through the configured mailer", async () => {
|
|
159
163
|
const templateCalls = [];
|
|
160
164
|
const mailerCalls = [];
|
|
165
|
+
let savedInvite;
|
|
161
166
|
const service = createService({
|
|
162
167
|
workspaceMembershipsRepository: {
|
|
163
168
|
async listActiveByWorkspaceId() {
|
|
@@ -165,6 +170,8 @@ test("workspaceMembersService.createInvite sends generated invite URLs through t
|
|
|
165
170
|
}
|
|
166
171
|
},
|
|
167
172
|
workspaceInvitesRepository: {
|
|
173
|
+
async withTransaction(work) { return work({}); },
|
|
174
|
+
async lockWorkspaceForInvitations() {},
|
|
168
175
|
async expirePendingByWorkspaceIdAndEmail(workspaceId, email) {
|
|
169
176
|
assert.equal(workspaceId, "7");
|
|
170
177
|
assert.equal(email, "invitee@example.com");
|
|
@@ -173,16 +180,14 @@ test("workspaceMembersService.createInvite sends generated invite URLs through t
|
|
|
173
180
|
assert.equal(payload.workspaceId, "7");
|
|
174
181
|
assert.equal(payload.email, "invitee@example.com");
|
|
175
182
|
assert.equal(payload.roleSid, "admin");
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
expiresAt: payload.expiresAt
|
|
179
|
-
};
|
|
183
|
+
savedInvite = { ...payload, id: "88", workspaceSlug: "acme", workspaceName: "Acme" };
|
|
184
|
+
return savedInvite;
|
|
180
185
|
},
|
|
181
186
|
async listPendingByWorkspaceIdWithWorkspace() {
|
|
182
187
|
return [];
|
|
183
188
|
},
|
|
184
189
|
async findPendingByIdForWorkspace() {
|
|
185
|
-
return
|
|
190
|
+
return savedInvite;
|
|
186
191
|
},
|
|
187
192
|
async revokeById() {}
|
|
188
193
|
},
|
|
@@ -253,6 +258,8 @@ test("workspaceMembersService.revokeInvite returns the revoked invite id", async
|
|
|
253
258
|
}
|
|
254
259
|
},
|
|
255
260
|
workspaceInvitesRepository: {
|
|
261
|
+
async withTransaction(work) { return work({}); },
|
|
262
|
+
async lockWorkspaceForInvitations() {},
|
|
256
263
|
async listPendingByWorkspaceIdWithWorkspace() {
|
|
257
264
|
return [];
|
|
258
265
|
},
|
|
@@ -300,6 +307,8 @@ test("workspaceMembersService rejects invite operations when invitations are dis
|
|
|
300
307
|
}
|
|
301
308
|
},
|
|
302
309
|
workspaceInvitesRepository: {
|
|
310
|
+
async withTransaction(work) { return work({}); },
|
|
311
|
+
async lockWorkspaceForInvitations() {},
|
|
303
312
|
async listPendingByWorkspaceIdWithWorkspace() {
|
|
304
313
|
throw new Error("invite repository should not be called when invitations are disabled");
|
|
305
314
|
},
|
|
@@ -440,6 +449,8 @@ test("workspaceMembersService.removeMember marks membership revoked and returns
|
|
|
440
449
|
}
|
|
441
450
|
},
|
|
442
451
|
workspaceInvitesRepository: {
|
|
452
|
+
async withTransaction(work) { return work({}); },
|
|
453
|
+
async lockWorkspaceForInvitations() {},
|
|
443
454
|
async listPendingByWorkspaceIdWithWorkspace() {
|
|
444
455
|
return [];
|
|
445
456
|
},
|
|
@@ -493,6 +504,8 @@ test("workspaceMembersService.removeMember rejects removing the owner", async ()
|
|
|
493
504
|
}
|
|
494
505
|
},
|
|
495
506
|
workspaceInvitesRepository: {
|
|
507
|
+
async withTransaction(work) { return work({}); },
|
|
508
|
+
async lockWorkspaceForInvitations() {},
|
|
496
509
|
async listPendingByWorkspaceIdWithWorkspace() {
|
|
497
510
|
return [];
|
|
498
511
|
},
|
|
@@ -16,6 +16,7 @@ function createFixture({
|
|
|
16
16
|
|
|
17
17
|
const service = createService({
|
|
18
18
|
workspaceInvitesRepository: {
|
|
19
|
+
async withTransaction(work) { return work({}); },
|
|
19
20
|
async listPendingByEmail() {
|
|
20
21
|
return Array.isArray(pendingInvitesByEmail) ? [...pendingInvitesByEmail] : [];
|
|
21
22
|
},
|
|
@@ -234,7 +235,7 @@ test("acceptInviteByToken accepts opaque invite token and resolves invite by dec
|
|
|
234
235
|
token: encodedToken
|
|
235
236
|
});
|
|
236
237
|
|
|
237
|
-
assert.deepEqual(calls.tokenHashCalls, [tokenHash]);
|
|
238
|
+
assert.deepEqual(calls.tokenHashCalls, [tokenHash, tokenHash]);
|
|
238
239
|
assert.equal(calls.upsertCalls.length, 1);
|
|
239
240
|
assert.deepEqual(calls.acceptCalls, [44]);
|
|
240
241
|
assert.deepEqual(calls.revokeCalls, []);
|
|
@@ -268,10 +269,37 @@ test("refuseInviteByToken revokes the invite and returns refused", async () => {
|
|
|
268
269
|
token: encodedToken
|
|
269
270
|
});
|
|
270
271
|
|
|
271
|
-
assert.deepEqual(calls.tokenHashCalls, [tokenHash]);
|
|
272
|
+
assert.deepEqual(calls.tokenHashCalls, [tokenHash, tokenHash]);
|
|
272
273
|
assert.deepEqual(calls.acceptCalls, []);
|
|
273
274
|
assert.deepEqual(calls.revokeCalls, [45]);
|
|
274
275
|
assert.equal(calls.upsertCalls.length, 0);
|
|
275
276
|
assert.equal(response.decision, "refused");
|
|
276
277
|
assert.equal(response.workspaceId, "1");
|
|
277
278
|
});
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
test("participant registration rejects invalid, duplicate and late registration", async () => {
|
|
282
|
+
const { service } = createFixture();
|
|
283
|
+
assert.throws(() => service.registerAcceptanceParticipant(null), /must be a function/);
|
|
284
|
+
service.registerAcceptanceParticipant(async () => {});
|
|
285
|
+
assert.throws(() => service.registerAcceptanceParticipant(async () => {}), /already registered/);
|
|
286
|
+
const { service: late } = createFixture();
|
|
287
|
+
await assert.rejects(late.acceptInviteByToken(), /Authentication required/);
|
|
288
|
+
assert.throws(() => late.registerAcceptanceParticipant(async () => {}), /registration is closed/);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
for (const transactionOutcome of ["committed", "unknown", "rolledBack"]) {
|
|
292
|
+
test(`acceptance preserves a ${transactionOutcome} managed completion failure`, async () => {
|
|
293
|
+
const cause = new Error("Completion failed");
|
|
294
|
+
const failure = Object.assign(new Error("Managed completion failed"), { transactionOutcome, cause });
|
|
295
|
+
const service = createService({
|
|
296
|
+
workspaceInvitesRepository: {
|
|
297
|
+
async findPendingByTokenHash() { return { workspaceId: "1", email: "a@example.test" }; },
|
|
298
|
+
async withTransaction() { throw failure; }
|
|
299
|
+
},
|
|
300
|
+
workspaceMembershipsRepository: {}
|
|
301
|
+
});
|
|
302
|
+
await assert.rejects(service.acceptInviteByToken({ user: { id: "1", email: "a@example.test" }, token: "valid-token" }),
|
|
303
|
+
(error) => error === failure && error.cause === cause);
|
|
304
|
+
});
|
|
305
|
+
}
|