@7365admin1/core 3.59.2 → 3.60.0

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.59.2",
4
+ "version": "3.60.0",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -362,6 +362,39 @@ describe("pre-login resident sign-up cannot manufacture access", { concurrency:
362
362
  assert.equal(file.status, "active");
363
363
  });
364
364
 
365
+ record("D. a double-tapped Confirm cannot damage the registration that won", async () => {
366
+ // Defect 4 in the field: nothing disabled the Confirm button, so every tap
367
+ // sent another POST. This measures what the SECOND one actually does.
368
+ const email = "pin-double@e2e.example.com";
369
+ const fileId = new ObjectId();
370
+ await h.db.collection("files").insertOne({ _id: fileId, name: "lease.pdf", status: "draft" });
371
+
372
+ const body = {
373
+ ...genuine(id, email),
374
+ files: [{ id: fileId.toString(), name: "lease.pdf", mimeType: "application/pdf" }],
375
+ };
376
+
377
+ const first = await h.api("/people/resident/create", { method: "POST", body });
378
+ const second = await h.api("/people/resident/create", { method: "POST", body });
379
+ const third = await h.api("/people/resident/create", { method: "POST", body });
380
+
381
+ assert.equal(first.status, 201, JSON.stringify(first.status));
382
+ // Every later tap dies on the duplicate-account check...
383
+ assert.equal(second.status, 400, JSON.stringify(second.body));
384
+ assert.equal(third.status, 400, JSON.stringify(third.body));
385
+ assert.match(JSON.stringify(second.body), /User already exists/);
386
+
387
+ // ...and the registration that won is untouched: one row, its document
388
+ // still linked and still active. A double submit is NOT what empties Files.
389
+ const rows = await h.db.collection("site.people").find({ email }).toArray();
390
+ assert.equal(rows.length, 1);
391
+ assert.equal(rows[0].files?.length, 1);
392
+ assert.equal(rows[0].files[0].id?.toString(), fileId.toString());
393
+ assert.equal("filesNotUploaded" in rows[0], false);
394
+ assert.equal((await h.db.collection("files").findOne({ _id: fileId })).status, "active");
395
+ assert.equal(await h.db.collection("users").countDocuments({ email }), 1);
396
+ });
397
+
365
398
  record("15. the caller cannot plant the report itself", async () => {
366
399
  const email = "pin-planted@e2e.example.com";
367
400
  const res = await h.api("/people/resident/create", {
@@ -0,0 +1,282 @@
1
+ /**
2
+ * The default-role SELF-HEAL: a write triggered by a read.
3
+ *
4
+ * `seedDefaultOrgRoles` only runs when an organisation is CREATED, so the 73
5
+ * organisations that were created before it shipped still hold not one role.
6
+ * `healOrgDefaultRoles` gives them the same two roles the first time somebody
7
+ * reads their empty roles list.
8
+ *
9
+ * That is an insert on a read path in a live multi-tenant system running on
10
+ * more than one instance, so every property that makes it safe is asserted
11
+ * here, and each one is a way this could take the platform down:
12
+ *
13
+ * 1. it fires ONLY on an organisation holding zero roles — one role means
14
+ * skip, no exceptions, the same rule the backfill tool uses;
15
+ * 2. two simultaneous readers of the same organisation produce TWO roles, not
16
+ * four — the claim is the guard, not a non-atomic read;
17
+ * 3. it never emits an `org`-less role: that is a CLIENT TEMPLATE, one
18
+ * document shared by every client invited with it, and one of those is
19
+ * what took every client's modules away on 2026-09-07;
20
+ * 4. it cannot fail the read it hangs off — a claim, count, nature lookup or
21
+ * insert that throws returns 0 and throws nothing;
22
+ * 5. it is insert-only: no role, member, invitation, user or organisation is
23
+ * updated, deleted or repointed.
24
+ *
25
+ * Nothing here opens a socket or a database. Every dependency is passed in,
26
+ * which is also how the production code is written.
27
+ */
28
+
29
+ import { strict as assert } from "node:assert";
30
+ import test from "node:test";
31
+ import { readFileSync } from "node:fs";
32
+
33
+ import { healOrgDefaultRoles } from "./.build/utils/org-default-roles.util.mjs";
34
+
35
+ const ORG = "68b0c1d2e3f4a5b6c7d8e9f0";
36
+
37
+ const tick = () => new Promise((resolve) => setImmediate(resolve));
38
+
39
+ /**
40
+ * A stand-in for `org-role-seed.repo.ts`.
41
+ *
42
+ * `claimSeed` is an `insertOne` of a row whose `_id` IS the organisation id, so
43
+ * MongoDB decides the winner atomically, inside the collection's `_id` index,
44
+ * before any caller can observe anything. The fake therefore decides and
45
+ * records SYNCHRONOUSLY and only then yields — modelling the database, not a
46
+ * read-then-write in application code, which is precisely the thing that would
47
+ * not be safe.
48
+ */
49
+ function fakeWorld({
50
+ roleCount = 0,
51
+ nature = "security_agency",
52
+ failOn = null,
53
+ } = {}) {
54
+ const claimed = new Set();
55
+ const written = [];
56
+ const released = [];
57
+
58
+ const boom = (name) => {
59
+ if (failOn === name) throw new Error(`${name} exploded`);
60
+ };
61
+
62
+ return {
63
+ written,
64
+ released,
65
+ claimed,
66
+ deps: {
67
+ async claim(org) {
68
+ boom("claim");
69
+ const key = String(org);
70
+ const won = !claimed.has(key);
71
+ claimed.add(key);
72
+ await tick();
73
+ return won;
74
+ },
75
+ async release(org) {
76
+ released.push(String(org));
77
+ claimed.delete(String(org));
78
+ },
79
+ async countRoles() {
80
+ boom("countRoles");
81
+ await tick();
82
+ return roleCount;
83
+ },
84
+ async getNature() {
85
+ boom("getNature");
86
+ await tick();
87
+ return nature;
88
+ },
89
+ async addRole(role) {
90
+ boom("addRole");
91
+ await tick();
92
+ written.push(role);
93
+ return role;
94
+ },
95
+ },
96
+ };
97
+ }
98
+
99
+ test("an organisation holding zero roles gets the same two roles a new one gets", async () => {
100
+ const world = fakeWorld();
101
+
102
+ const seeded = await healOrgDefaultRoles(world.deps, ORG);
103
+
104
+ assert.equal(seeded, 2, "two roles, exactly as a paid signup writes");
105
+ assert.equal(world.written.length, 2);
106
+
107
+ for (const role of world.written) {
108
+ assert.equal(role.name, "owner");
109
+ assert.deepEqual(role.permissions, ["*"]);
110
+ assert.equal(role.default, true, "the repository path writes `default: true`");
111
+ assert.equal(role.org, ORG, "every seeded role carries the organisation");
112
+ }
113
+
114
+ assert.deepEqual(
115
+ world.written.map((r) => r.type).sort(),
116
+ ["organization", "security_agency"],
117
+ "one organisation role and one typed by the organisation's nature",
118
+ );
119
+ });
120
+
121
+ test("an organisation holding one role is left completely alone", async () => {
122
+ const world = fakeWorld({ roleCount: 1 });
123
+
124
+ const seeded = await healOrgDefaultRoles(world.deps, ORG);
125
+
126
+ assert.equal(seeded, 0, "one role means skip — no exceptions");
127
+ assert.deepEqual(world.written, [], "nothing was written");
128
+ });
129
+
130
+ test("the count is re-read UNDER the claim, so a role created meanwhile still stops it", async () => {
131
+ // The caller's empty list can be up to 15 minutes stale (Redis), and a role
132
+ // can be created between that list and this insert. The count that decides
133
+ // is the one taken after the claim is won, straight from the collection.
134
+ let roles = 0;
135
+ const world = fakeWorld();
136
+ world.deps.countRoles = async () => {
137
+ await tick();
138
+ return roles;
139
+ };
140
+ roles = 1;
141
+
142
+ assert.equal(await healOrgDefaultRoles(world.deps, ORG), 0);
143
+ assert.deepEqual(world.written, []);
144
+ });
145
+
146
+ test("a second load of the same organisation is a no-op", async () => {
147
+ const world = fakeWorld();
148
+
149
+ assert.equal(await healOrgDefaultRoles(world.deps, ORG), 2);
150
+ assert.equal(await healOrgDefaultRoles(world.deps, ORG), 0, "the claim is already held");
151
+ assert.equal(world.written.length, 2, "still two roles, not four");
152
+ });
153
+
154
+ test("two simultaneous loads of the same organisation do not create four roles", async () => {
155
+ // Both callers see a count of zero — the exact race the claim exists for.
156
+ const world = fakeWorld({ roleCount: 0 });
157
+
158
+ const results = await Promise.all([
159
+ healOrgDefaultRoles(world.deps, ORG),
160
+ healOrgDefaultRoles(world.deps, ORG),
161
+ ]);
162
+
163
+ assert.deepEqual(
164
+ results.sort(),
165
+ [0, 2],
166
+ "exactly one caller seeded; the other did nothing at all",
167
+ );
168
+ assert.equal(world.written.length, 2, "two roles in total, not four");
169
+ });
170
+
171
+ test("ten simultaneous loads still produce exactly one set", async () => {
172
+ const world = fakeWorld({ roleCount: 0 });
173
+
174
+ const results = await Promise.all(
175
+ Array.from({ length: 10 }, () => healOrgDefaultRoles(world.deps, ORG)),
176
+ );
177
+
178
+ assert.equal(
179
+ results.filter((n) => n > 0).length,
180
+ 1,
181
+ "one winner out of ten",
182
+ );
183
+ assert.equal(world.written.length, 2);
184
+ });
185
+
186
+ test("it never emits an org-less role", async () => {
187
+ for (const org of [null, undefined, ""]) {
188
+ const world = fakeWorld();
189
+
190
+ assert.equal(await healOrgDefaultRoles(world.deps, org), 0);
191
+ assert.deepEqual(world.written, [], "no organisation, no role — ever");
192
+ assert.equal(world.claimed.size, 0, "it does not even take a claim");
193
+ }
194
+ });
195
+
196
+ test("a nature that is not an app produces the organisation role only, still org-scoped", async () => {
197
+ const world = fakeWorld({ nature: "property_owner" });
198
+
199
+ assert.equal(await healOrgDefaultRoles(world.deps, ORG), 1);
200
+ assert.equal(world.written[0].type, "organization");
201
+ assert.equal(world.written[0].org, ORG);
202
+ });
203
+
204
+ test("an insert that fails does not break the read, and gives the claim back", async () => {
205
+ const world = fakeWorld({ failOn: "addRole" });
206
+
207
+ const seeded = await healOrgDefaultRoles(world.deps, ORG);
208
+
209
+ assert.equal(seeded, 0, "it resolves rather than rejecting");
210
+ assert.deepEqual(world.released, [ORG], "the claim is released so a later read may retry");
211
+ });
212
+
213
+ test("a count that fails does not break the read", async () => {
214
+ const world = fakeWorld({ failOn: "countRoles" });
215
+
216
+ assert.equal(await healOrgDefaultRoles(world.deps, ORG), 0);
217
+ assert.deepEqual(world.written, []);
218
+ assert.deepEqual(world.released, [ORG]);
219
+ });
220
+
221
+ test("a claim that fails does not break the read, and nothing is released", async () => {
222
+ const world = fakeWorld({ failOn: "claim" });
223
+
224
+ assert.equal(await healOrgDefaultRoles(world.deps, ORG), 0);
225
+ assert.deepEqual(world.written, []);
226
+ assert.deepEqual(world.released, [], "no claim was taken, so there is none to give back");
227
+ });
228
+
229
+ test("a nature lookup that fails does not break the read", async () => {
230
+ const world = fakeWorld({ failOn: "getNature" });
231
+
232
+ assert.equal(await healOrgDefaultRoles(world.deps, ORG), 0);
233
+ assert.deepEqual(world.written, []);
234
+ });
235
+
236
+ test("a release that itself fails is still swallowed", async () => {
237
+ const world = fakeWorld({ failOn: "addRole" });
238
+ world.deps.release = async () => {
239
+ throw new Error("release exploded");
240
+ };
241
+
242
+ assert.equal(
243
+ await healOrgDefaultRoles(world.deps, ORG),
244
+ 0,
245
+ "the read survives even when the cleanup cannot run",
246
+ );
247
+ });
248
+
249
+ test("the trigger fires on a type-filtered list, and not on a search, a site or page two", () => {
250
+ // The write hangs off `role.controller.ts getRoles`.
251
+ //
252
+ // `type` is deliberately ALLOWED. Every live call site passes one —
253
+ // `RolePermissionMain type="organization"`, `InvitationForm props.app`,
254
+ // `MemberMain props.type` — so a trigger that required an unfiltered list
255
+ // would never fire in production, and the whole self-heal would be dead code
256
+ // that reads exactly like a working one. An empty type-filtered page is not
257
+ // proof of zero roles, and it is not asked to be: the unfiltered count taken
258
+ // UNDER the claim is what decides, and that is asserted above.
259
+ //
260
+ // A search miss is user-driven and high-cardinality, a site-scoped page is
261
+ // empty for organisation-wide roles that plainly exist, and page two of
262
+ // anything is empty by arithmetic. None of those may trigger a write.
263
+ const source = readFileSync(
264
+ new URL("../src/controllers/role.controller.ts", import.meta.url),
265
+ "utf8",
266
+ );
267
+
268
+ const guard = source.slice(
269
+ source.indexOf("const listIsWholeOrg ="),
270
+ source.indexOf("const seeded = await healDefaultRoles("),
271
+ );
272
+
273
+ assert.ok(guard.includes("org &&"), "only when an organisation was named");
274
+ assert.ok(guard.includes("!search"), "not a search result");
275
+ assert.ok(guard.includes("!site"), "not a site-scoped list");
276
+ assert.ok(guard.includes("page === 1"), "not page two of a list");
277
+ assert.ok(guard.includes("?.length === 0"), "only when the page is empty");
278
+ assert.ok(
279
+ !guard.includes("!type"),
280
+ "a type filter must NOT block the heal - every live call site sends one",
281
+ );
282
+ });