@7365admin1/core 3.68.1 → 3.68.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@7365admin1/core",
3
3
  "license": "MIT",
4
- "version": "3.68.1",
4
+ "version": "3.68.3",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -0,0 +1,138 @@
1
+ // Re-inviting the same person must not add a second pending invitation.
2
+ //
3
+ // `createSimpleUserInvite` (POST /api/auth/invite/user — the console's Invite
4
+ // Client form and the org/hygiene invite screens) looked for an existing
5
+ // invitation and threw the answer away, so every click added another pending
6
+ // row and another e-mail. Now a pending invitation for the same person, app,
7
+ // organisation and site is sent again on the SAME row: fresh expiry, the new
8
+ // request's role. Accepted or cancelled invitations are left alone.
9
+ //
10
+ // In-process MongoDB, loopback Redis and a loopback mail sink only (harness.mjs).
11
+ //
12
+ // Run with: yarn test:e2e
13
+
14
+ import { after, before, describe, it } from "node:test";
15
+ import assert from "node:assert/strict";
16
+ import { ObjectId } from "mongodb";
17
+
18
+ import { startHarness } from "./harness.mjs";
19
+
20
+ const PASSWORD = "InviteDedupe-Passw0rd!";
21
+ const STAFF = "sevenadmin-dedupe@e2e.example.com";
22
+ const OWNER = "owner-dedupe@e2e.example.com";
23
+ const CLIENT = "Client-Dedupe@e2e.example.com";
24
+
25
+ describe("re-inviting reuses the pending invitation", { concurrency: 1 }, () => {
26
+ let h;
27
+ const id = {};
28
+ let staffSid;
29
+ let ownerSid;
30
+
31
+ before(async () => {
32
+ h = await startHarness();
33
+ Object.assign(id, await seed(h));
34
+ staffSid = await h.login(STAFF, PASSWORD);
35
+ ownerSid = await h.login(OWNER, PASSWORD);
36
+ }, { timeout: 300000 });
37
+
38
+ after(async () => {
39
+ if (h) await h.stop();
40
+ });
41
+
42
+ const invites = (email) =>
43
+ h.db
44
+ .collection("verifications")
45
+ .find({ type: "user-invite", email: new RegExp(`^${email}$`, "i") })
46
+ .toArray();
47
+
48
+ const invite = (sid, body) =>
49
+ h.api("/auth/invite/user", { method: "POST", sid, body });
50
+
51
+ it("the console's client invite, sent twice, is one row and the same link", async () => {
52
+ const body = { email: CLIENT, app: ["security_agency"], role: id.templateRole.toString(), name: "Org Owner" };
53
+
54
+ const first = await invite(staffSid, body);
55
+ assert.equal(first.status, 201, JSON.stringify(first.body));
56
+
57
+ const sent = h.mails.length;
58
+ // a different letter case is the same person
59
+ const second = await invite(staffSid, { ...body, email: CLIENT.toLowerCase() });
60
+ assert.equal(second.status, 201, JSON.stringify(second.body));
61
+
62
+ const rows = await invites(CLIENT);
63
+ assert.equal(rows.length, 1, "no second pending invitation");
64
+ assert.deepEqual(second.body.verificationIds, first.body.verificationIds);
65
+
66
+ // the e-mail is sent again, carrying the same link
67
+ const mail = await h.mailAt(sent);
68
+ assert.ok(mail.body.includes(first.body.verificationIds[0]), "resent mail links the same invitation");
69
+ });
70
+
71
+ it("an organisation invite sent again takes the new role", async () => {
72
+ const body = {
73
+ email: "member-dedupe@e2e.example.com",
74
+ app: ["organization"],
75
+ org: id.org.toString(),
76
+ siteId: id.site.toString(),
77
+ siteName: "Dedupe Site",
78
+ role: id.roleA.toString(),
79
+ };
80
+
81
+ assert.equal((await invite(ownerSid, body)).status, 201);
82
+ assert.equal((await invite(ownerSid, { ...body, role: id.roleB.toString() })).status, 201);
83
+
84
+ const rows = await invites(body.email);
85
+ assert.equal(rows.length, 1);
86
+ assert.equal(rows[0].metadata.role.toString(), id.roleB.toString());
87
+ assert.equal(rows[0].metadata.org.toString(), id.org.toString(), "still stored as the organisation id");
88
+ });
89
+
90
+ it("a different app, or an invitation already accepted, still creates a new one", async () => {
91
+ const body = { email: "multi-dedupe@e2e.example.com", app: ["security_agency"], role: id.templateRole.toString() };
92
+
93
+ await invite(staffSid, body);
94
+ await invite(staffSid, { ...body, app: ["cleaning_services"] });
95
+ assert.equal((await invites(body.email)).length, 2);
96
+
97
+ await h.db.collection("verifications").updateMany({ email: body.email }, { $set: { status: "complete" } });
98
+ await invite(staffSid, body);
99
+ assert.equal((await invites(body.email)).length, 3);
100
+ });
101
+ });
102
+
103
+ async function seed(h) {
104
+ const now = new Date().toISOString();
105
+ const org = new ObjectId();
106
+ const site = new ObjectId();
107
+ const staffRole = new ObjectId();
108
+ const templateRole = new ObjectId();
109
+ const roleA = new ObjectId();
110
+ const roleB = new ObjectId();
111
+
112
+ await h.db.collection("organizations").insertOne({
113
+ _id: org, name: "Dedupe Org", email: OWNER, type: "org", nature: "property_management_agency", status: "active", createdAt: now,
114
+ });
115
+ await h.db.collection("sites").insertOne({ _id: site, name: "Dedupe Site", org, orgId: org, status: "active" });
116
+ await h.db.collection("roles").insertMany([
117
+ { _id: staffRole, name: "Super Admin", type: "admin", default: true, permissions: [], status: "active" },
118
+ { _id: templateRole, name: "Org Owner", type: "organization", permissions: ["*"], status: "active" },
119
+ { _id: roleA, name: "Supervisor", org, permissions: ["*"], status: "active" },
120
+ { _id: roleB, name: "Manager", org, permissions: ["*"], status: "active" },
121
+ ]);
122
+ await h.db.collection("subscriptions").insertOne({
123
+ org, type: "organization", status: "active", maxSeats: 0, paidSeats: 0, currentSeats: 0, createdAt: now,
124
+ });
125
+
126
+ const users = await h.db.collection("users").insertMany(
127
+ [STAFF, OWNER].map((email) => ({ email, name: email.split("@")[0], status: "active", createdAt: now })),
128
+ );
129
+ const hashed = await h.hashPassword(PASSWORD);
130
+ await h.db.collection("users").updateMany({}, { $set: { password: hashed } });
131
+
132
+ await h.db.collection("members").insertMany([
133
+ { user: users.insertedIds[0], type: "admin", role: staffRole, status: "active" },
134
+ { user: users.insertedIds[1], org, type: "organization", role: roleA, status: "active" },
135
+ ]);
136
+
137
+ return { org, site, templateRole, roleA, roleB };
138
+ }
@@ -0,0 +1,30 @@
1
+ /*
2
+ * A duplicate membership must read as "already a member" (400), not a 500.
3
+ *
4
+ * The live unique index on `members` is `org_1_siteId_1_user_1_type_1`; the old
5
+ * check only recognised a message naming `org_1_user_1`, so every real duplicate
6
+ * fell through as a raw MongoServerError.
7
+ */
8
+ import { strict as assert } from "node:assert";
9
+ import test from "node:test";
10
+
11
+ import { isDuplicateMemberError } from "./.build/models/member.model.mjs";
12
+
13
+ test("the live unique index's duplicate is recognised", () => {
14
+ const err = Object.assign(
15
+ new Error(
16
+ 'E11000 duplicate key error collection: iservice365.members index: org_1_siteId_1_user_1_type_1 dup key: { org: ObjectId("6a951ce603be44033d434f68") }',
17
+ ),
18
+ { code: 11000 },
19
+ );
20
+ assert.equal(isDuplicateMemberError(err), true);
21
+ });
22
+
23
+ test("a duplicate is recognised by code alone", () => {
24
+ assert.equal(isDuplicateMemberError({ code: 11000, message: "" }), true);
25
+ });
26
+
27
+ test("other errors are left alone", () => {
28
+ assert.equal(isDuplicateMemberError(new Error("connection reset")), false);
29
+ assert.equal(isDuplicateMemberError(undefined), false);
30
+ });