@7365admin1/core 3.59.1 → 3.59.2

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.1",
4
+ "version": "3.59.2",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -309,6 +309,71 @@ describe("pre-login resident sign-up cannot manufacture access", { concurrency:
309
309
  assert.equal(person?.status, "active");
310
310
  assert.equal(person?.remarks, "e2e staff may set this");
311
311
  });
312
+
313
+ // ---- a document that never uploaded is dropped, AND reported -------------
314
+
315
+ record("13. a sign-up whose document upload failed still completes", async () => {
316
+ // Exactly what the wizard sends after a failed upload: the file is named
317
+ // and listed, and has no `id` because the upload never returned one.
318
+ const email = "pin-unsent@e2e.example.com";
319
+ const res = await h.api("/people/resident/create", {
320
+ method: "POST",
321
+ body: {
322
+ ...genuine(id, email),
323
+ files: [{ name: "tenancy-agreement.pdf", mimeType: "application/pdf" }],
324
+ },
325
+ });
326
+
327
+ assert.equal(res.status, 201, JSON.stringify(res.status));
328
+
329
+ const person = await stored(email);
330
+ assert.ok(person, "the applicant must not be lost over an attachment we never received");
331
+ assert.deepEqual(person.files, []);
332
+ // ...and the reviewer can now tell this apart from "attached nothing".
333
+ assert.deepEqual(person.filesNotUploaded, ["tenancy-agreement.pdf"]);
334
+ });
335
+
336
+ record("14. a sign-up whose uploads all landed carries no report at all", async () => {
337
+ const email = "pin-uploaded@e2e.example.com";
338
+ const fileId = new ObjectId();
339
+ await h.db.collection("files").insertOne({
340
+ _id: fileId,
341
+ name: "tenancy-agreement.pdf",
342
+ status: "draft",
343
+ });
344
+
345
+ const res = await h.api("/people/resident/create", {
346
+ method: "POST",
347
+ body: {
348
+ ...genuine(id, email),
349
+ files: [{ id: fileId.toString(), name: "tenancy-agreement.pdf", mimeType: "application/pdf" }],
350
+ },
351
+ });
352
+
353
+ assert.equal(res.status, 201, JSON.stringify(res.status));
354
+
355
+ const person = await stored(email);
356
+ assert.equal(person.files?.length, 1);
357
+ assert.equal(person.files[0].id?.toString(), fileId.toString());
358
+ // Absent, not empty: a normal registration is written exactly as before.
359
+ assert.equal("filesNotUploaded" in person, false);
360
+ // And the document was linked - its status flipped out of draft.
361
+ const file = await h.db.collection("files").findOne({ _id: fileId });
362
+ assert.equal(file.status, "active");
363
+ });
364
+
365
+ record("15. the caller cannot plant the report itself", async () => {
366
+ const email = "pin-planted@e2e.example.com";
367
+ const res = await h.api("/people/resident/create", {
368
+ method: "POST",
369
+ body: { ...genuine(id, email), filesNotUploaded: ["planted-by-the-caller.pdf"] },
370
+ });
371
+
372
+ assert.equal(res.status, 201, JSON.stringify(res.status));
373
+
374
+ const person = await stored(email);
375
+ assert.equal("filesNotUploaded" in person, false);
376
+ });
312
377
  });
313
378
 
314
379
  async function seed(h) {
@@ -3,8 +3,13 @@ import test from "node:test";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
- import { dropUnsentFiles } from "./.build/utils/self-signup-files.util.mjs";
7
- import { schemaResidentSelfSignUp, schemaPerson } from "./.build/models/person.model.mjs";
6
+ import {
7
+ dropUnsentFiles,
8
+ unsentFileNames,
9
+ MAX_UNSENT_FILE_NAMES,
10
+ MAX_UNSENT_FILE_NAME_LENGTH,
11
+ } from "./.build/utils/self-signup-files.util.mjs";
12
+ import { schemaResidentSelfSignUp, schemaPerson, MPerson } from "./.build/models/person.model.mjs";
8
13
 
9
14
  /**
10
15
  * Resident sign-up outage, 2026-09-08.
@@ -150,5 +155,122 @@ test("the filter runs on the self-signup path, before validation, and nowhere el
150
155
  // authenticated `add`/`updateById` paths, staff stop being told about a
151
156
  // client that is silently losing their attachments.
152
157
  assert.equal(controller.match(/dropUnsentFiles\(/g)?.length, 1);
153
- assert.match(controller, /import \{ dropUnsentFiles \} from "\.\.\/utils\/self-signup-files\.util"/);
158
+ // Both helpers come from that one module, whatever shape the import takes.
159
+ const importBlock = controller.match(/import \{[\s\S]*?\} from "\.\.\/utils\/self-signup-files\.util";/);
160
+ assert.ok(importBlock, "self-signup-files.util is no longer imported by the controller");
161
+ assert.ok(importBlock[0].includes("dropUnsentFiles"));
162
+ assert.ok(importBlock[0].includes("unsentFileNames"));
163
+ });
164
+
165
+ /**
166
+ * 2026-09-08, second report. The fix above kept the applicant, and then hid the
167
+ * problem: `Files:` renders empty whether the resident attached nothing or
168
+ * attached something we never received. The owner's own registration was
169
+ * APPROVED in that state, without its tenancy agreement. So the entries we drop
170
+ * are now recorded on the person as `filesNotUploaded`, and the console has
171
+ * something to show.
172
+ */
173
+
174
+ test("what was dropped is reported, by name, in order", () => {
175
+ const second = { name: "vehicle-registration.pdf", mimeType: "application/pdf" };
176
+ const body = { ...base(), files: [validFile, unsentFile, second] };
177
+
178
+ assert.deepEqual(unsentFileNames(body), [unsentFile.name, second.name]);
179
+ // And the two halves agree: what is kept plus what is reported is the input.
180
+ assert.deepEqual(dropUnsentFiles(body).files, [validFile]);
181
+ });
182
+
183
+ test("a registration where every upload landed reports nothing", () => {
184
+ assert.deepEqual(unsentFileNames({ ...base(), files: [validFile] }), []);
185
+ assert.deepEqual(unsentFileNames({ ...base(), files: [] }), []);
186
+ assert.deepEqual(unsentFileNames(base()), []);
187
+ assert.deepEqual(unsentFileNames({ ...base(), files: null }), []);
188
+ assert.deepEqual(unsentFileNames({ ...base(), files: "nonsense" }), []);
189
+ assert.deepEqual(unsentFileNames(undefined), []);
190
+ });
191
+
192
+ test("an unnamed dropped entry still reports something a reviewer can read", () => {
193
+ assert.deepEqual(unsentFileNames({ files: [{ mimeType: "application/pdf" }] }), ["Unnamed document"]);
194
+ assert.deepEqual(unsentFileNames({ files: [{ name: " " }] }), ["Unnamed document"]);
195
+ assert.deepEqual(unsentFileNames({ files: [{ name: 42 }] }), ["Unnamed document"]);
196
+ // A null entry is not an unsent file, it is nonsense - and must not crash.
197
+ assert.deepEqual(unsentFileNames({ files: [null, undefined] }), []);
198
+ });
199
+
200
+ test("the caller cannot use it as free storage: bounded in count and in length", () => {
201
+ const many = Array.from({ length: MAX_UNSENT_FILE_NAMES + 5 }, (_, i) => ({ name: `doc-${i}.pdf` }));
202
+ assert.equal(unsentFileNames({ files: many }).length, MAX_UNSENT_FILE_NAMES);
203
+
204
+ const long = unsentFileNames({ files: [{ name: "x".repeat(5000) }] });
205
+ assert.equal(long[0].length, MAX_UNSENT_FILE_NAME_LENGTH);
206
+ });
207
+
208
+ test("the report is display text only - it is not accepted from the caller", () => {
209
+ // stripUnknown means a caller who sends `filesNotUploaded` has it dropped,
210
+ // exactly like `status` and `platform`. Only the controller sets it.
211
+ const { error, value } = selfSignUp({ ...base(), filesNotUploaded: ["planted.pdf"] });
212
+ assert.equal(error, undefined);
213
+ assert.equal("filesNotUploaded" in value, false);
214
+
215
+ // But the finished payload the controller builds must survive MPerson, which
216
+ // runs schemaPerson over it and refuses unknown keys.
217
+ const finished = { ...base(), isOwner: true, status: "pending", platform: "mobile", filesNotUploaded: ["a.pdf"] };
218
+ assert.equal(schemaPerson.validate(finished, { abortEarly: false }).error, undefined);
219
+ });
220
+
221
+ test("the controller records the dropped names from the ORIGINAL body, and only when there are some", () => {
222
+ const selfSignUpFn = controller.slice(
223
+ controller.indexOf("async function addResidentSelfSignUp"),
224
+ controller.indexOf("async function add("),
225
+ );
226
+
227
+ assert.match(selfSignUpFn, /unsentFileNames\(req\.body\)/);
228
+ // Read from req.body, not from the already-filtered `body` - which would
229
+ // always be empty and make the whole thing a silent no-op.
230
+ assert.equal(selfSignUpFn.includes("unsentFileNames(body)"), false);
231
+ assert.match(selfSignUpFn, /filesNotUploaded: notUploaded/);
232
+ assert.match(selfSignUpFn, /notUploaded\.length > 0 \?/);
233
+
234
+ // Self-signup only. The authenticated staff path must not start writing it.
235
+ assert.equal(controller.match(/unsentFileNames\(/g)?.length, 1);
236
+ });
237
+
238
+ /**
239
+ * `MPerson` does not return the object it was given: it returns an explicit
240
+ * whitelist, and anything not named there is dropped on the way to the
241
+ * database. Every unit test above passed with `filesNotUploaded` never reaching
242
+ * Mongo at all - only the end-to-end run caught it. Pin it here so the next
243
+ * field added to the payload is not lost the same way.
244
+ */
245
+ test("MPerson carries the report through to the document it writes", () => {
246
+ const person = {
247
+ name: "Resident Test",
248
+ email: "resident@example.com",
249
+ isOwner: true,
250
+ site: "0123456789abcdef01234567",
251
+ status: "pending",
252
+ platform: "mobile",
253
+ files: [],
254
+ filesNotUploaded: ["tenancy-agreement.pdf"],
255
+ };
256
+
257
+ assert.deepEqual(MPerson(person).filesNotUploaded, ["tenancy-agreement.pdf"]);
258
+ });
259
+
260
+ test("a person with nothing to report gets no key at all, not an undefined one", () => {
261
+ // A fresh object each time: MPerson rewrites `site` into an ObjectId in
262
+ // place, so a second call on the same literal fails on "site must be a string".
263
+ const person = (extra) => ({
264
+ name: "Resident Test",
265
+ email: "resident@example.com",
266
+ isOwner: true,
267
+ site: "0123456789abcdef01234567",
268
+ files: [],
269
+ ...extra,
270
+ });
271
+
272
+ // `filesNotUploaded: undefined` would be written into every person document
273
+ // ever created, on every path, which is exactly what the conditional avoids.
274
+ assert.equal("filesNotUploaded" in MPerson(person()), false);
275
+ assert.equal("filesNotUploaded" in MPerson(person({ filesNotUploaded: [] })), false);
154
276
  });