@7365admin1/core 3.59.0 → 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/CHANGELOG.md +20 -0
- package/dist/index.d.ts +13 -5
- package/dist/index.js +35 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +35 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/e2e/resident-self-signup-pinning.e2e.test.mjs +65 -0
- package/test/self-signup-files.test.mjs +276 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7365admin1/core",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "3.59.
|
|
4
|
+
"version": "3.59.2",
|
|
5
5
|
"author": "7365admin1",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build": "tsup src/index.ts --format cjs,esm --dts && shx cp -r src/public dist && shx cp -r src/utils/handlebars dist",
|
|
14
14
|
"release": "yarn run build && changeset publish",
|
|
15
15
|
"lint": "tsc",
|
|
16
|
-
"test": "tsup src/config.ts src/utils/anpr-revoke.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/models/guard-invite.model.ts src/utils/site-timezone.util.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts src/models/vehicle.model.ts src/controllers/chat.controller.ts src/utils/file-owner.util.ts src/utils/console-permission.util.ts src/utils/role-scope.util.ts src/utils/org-default-roles.util.ts src/utils/resident-default-role.util.ts src/utils/invite-role-scope.util.ts src/models/personal-emergency-chain.model.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
|
|
16
|
+
"test": "tsup src/config.ts src/utils/anpr-revoke.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/models/guard-invite.model.ts src/utils/site-timezone.util.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts src/models/vehicle.model.ts src/controllers/chat.controller.ts src/utils/file-owner.util.ts src/utils/console-permission.util.ts src/utils/role-scope.util.ts src/utils/org-default-roles.util.ts src/utils/resident-default-role.util.ts src/utils/invite-role-scope.util.ts src/models/personal-emergency-chain.model.ts src/utils/self-signup-files.util.ts src/models/person.model.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
|
|
17
17
|
"test:camera": "yarn test",
|
|
18
18
|
"test:e2e": "yarn build && node --test --test-concurrency=1 --test-timeout=600000 \"test/e2e/*.test.mjs\""
|
|
19
19
|
},
|
|
@@ -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) {
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
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";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resident sign-up outage, 2026-09-08.
|
|
16
|
+
*
|
|
17
|
+
* A document whose upload failed stays in the app's list with no `id`
|
|
18
|
+
* (`UserDocumentStep.tsx:83-85` swallows the error), the wizard sends it, and
|
|
19
|
+
* `schemaFiles.id.required()` 400s the WHOLE registration with
|
|
20
|
+
* `"files[0].id" is required`. Measured against production: a body with no
|
|
21
|
+
* `files` reached the write, the same body with one id-less entry died at Joi.
|
|
22
|
+
* The resident sees nothing, because `createUser.tsx:458` only logs it.
|
|
23
|
+
*
|
|
24
|
+
* These pin the fix and, just as importantly, pin that it is STRICTLY WIDENING:
|
|
25
|
+
* everything that validated before still validates, identically.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// A body that is otherwise a perfectly good registration.
|
|
29
|
+
const base = () => ({
|
|
30
|
+
name: "Resident Test",
|
|
31
|
+
email: "resident@example.com",
|
|
32
|
+
password: "not-a-real-password",
|
|
33
|
+
type: "resident",
|
|
34
|
+
isOwner: true,
|
|
35
|
+
site: "0123456789abcdef01234567",
|
|
36
|
+
unit: "0123456789abcdef01234568",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const validFile = { id: "0123456789abcdef01234569", name: "ic-front.jpg", mimeType: "image/jpeg" };
|
|
40
|
+
// What the app sends after a failed upload: named, listed, and never uploaded.
|
|
41
|
+
const unsentFile = { name: "SEVEN365_Company_Profile_2026.pdf", mimeType: "application/pdf" };
|
|
42
|
+
|
|
43
|
+
const selfSignUp = (body) => schemaResidentSelfSignUp.validate(body, { abortEarly: false, stripUnknown: true });
|
|
44
|
+
|
|
45
|
+
test("the outage: a registration whose ONLY defect is an id-less file now goes through", () => {
|
|
46
|
+
const broken = { ...base(), files: [unsentFile] };
|
|
47
|
+
|
|
48
|
+
// Exactly the failure the owner's residents hit, before the fix.
|
|
49
|
+
const before = selfSignUp(broken);
|
|
50
|
+
assert.match(before.error?.message ?? "", /"files\[0\]\.id" is required/);
|
|
51
|
+
|
|
52
|
+
const after = selfSignUp(dropUnsentFiles(broken));
|
|
53
|
+
assert.equal(after.error, undefined);
|
|
54
|
+
// The applicant is kept, the attachment we never received is not.
|
|
55
|
+
assert.deepEqual(after.value.files, []);
|
|
56
|
+
assert.equal(after.value.email, "resident@example.com");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("a valid file id is still linked, byte for byte, in the same order", () => {
|
|
60
|
+
const second = { id: "0123456789abcdef0123456a", name: "ic-back.jpg", mimeType: "image/jpeg" };
|
|
61
|
+
const body = { ...base(), files: [validFile, second] };
|
|
62
|
+
|
|
63
|
+
const { error, value } = selfSignUp(dropUnsentFiles(body));
|
|
64
|
+
|
|
65
|
+
assert.equal(error, undefined);
|
|
66
|
+
assert.deepEqual(value.files, [validFile, second]);
|
|
67
|
+
// Nothing was reordered or rewritten - this is what the status-flip loop reads.
|
|
68
|
+
assert.equal(value.files[0].id, validFile.id);
|
|
69
|
+
assert.equal(value.files[1].id, second.id);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a mixed list keeps every real upload and drops only the failed one", () => {
|
|
73
|
+
const body = { ...base(), files: [validFile, unsentFile] };
|
|
74
|
+
|
|
75
|
+
const { error, value } = selfSignUp(dropUnsentFiles(body));
|
|
76
|
+
|
|
77
|
+
assert.equal(error, undefined);
|
|
78
|
+
assert.deepEqual(value.files, [validFile]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("a body with no files key is returned completely unchanged", () => {
|
|
82
|
+
const body = base();
|
|
83
|
+
const out = dropUnsentFiles(body);
|
|
84
|
+
|
|
85
|
+
assert.deepEqual(out, body);
|
|
86
|
+
// Not `files: undefined` - the key must not appear at all, or a later
|
|
87
|
+
// `"files" in body` check silently changes meaning.
|
|
88
|
+
assert.equal("files" in out, false);
|
|
89
|
+
assert.equal(selfSignUp(out).error, undefined);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("a files value that is not an array is handed to the schema untouched", () => {
|
|
93
|
+
// `null` is explicitly allowed by the schema and must stay allowed; a string
|
|
94
|
+
// is not, and must still be refused by Joi rather than quietly swallowed here.
|
|
95
|
+
assert.equal(dropUnsentFiles({ ...base(), files: null }).files, null);
|
|
96
|
+
assert.equal(selfSignUp(dropUnsentFiles({ ...base(), files: null })).error, undefined);
|
|
97
|
+
|
|
98
|
+
assert.equal(dropUnsentFiles({ ...base(), files: "nonsense" }).files, "nonsense");
|
|
99
|
+
assert.match(selfSignUp(dropUnsentFiles({ ...base(), files: "nonsense" })).error?.message ?? "", /files/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("an empty files array stays an empty files array", () => {
|
|
103
|
+
const { error, value } = selfSignUp(dropUnsentFiles({ ...base(), files: [] }));
|
|
104
|
+
assert.equal(error, undefined);
|
|
105
|
+
assert.deepEqual(value.files, []);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("the input body is not mutated", () => {
|
|
109
|
+
const body = { ...base(), files: [unsentFile] };
|
|
110
|
+
dropUnsentFiles(body);
|
|
111
|
+
assert.deepEqual(body.files, [unsentFile]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("the authenticated staff path is untouched - schemaPerson still refuses an id-less file", () => {
|
|
115
|
+
const staffBody = {
|
|
116
|
+
name: "Added By Staff",
|
|
117
|
+
email: "staff-added@example.com",
|
|
118
|
+
type: "resident",
|
|
119
|
+
site: "0123456789abcdef01234567",
|
|
120
|
+
files: [unsentFile],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// schemaPerson is what `add()` uses when there IS a caller. A staff client
|
|
124
|
+
// sending a file with no id is a bug in that client, and stays a 400.
|
|
125
|
+
assert.match(schemaPerson.validate(staffBody, { abortEarly: false }).error?.message ?? "", /"files\[0\]\.id" is required/);
|
|
126
|
+
// And it is refused for the same reason with the util applied, proving the
|
|
127
|
+
// util is not reachable from that path by accident.
|
|
128
|
+
assert.equal(schemaPerson.validate(staffBody, { abortEarly: false }).error !== undefined, true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The decision above is pure. What it cannot show is the WIRING: constructing
|
|
133
|
+
* the controller opens a live Mongo handle, so the only way to prove the filter
|
|
134
|
+
* runs on the self-signup path - and ONLY there - is to assert the source.
|
|
135
|
+
*/
|
|
136
|
+
const controller = readFileSync(
|
|
137
|
+
fileURLToPath(new URL("../src/controllers/person.controller.ts", import.meta.url)),
|
|
138
|
+
"utf8",
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
test("the filter runs on the self-signup path, before validation, and nowhere else", () => {
|
|
142
|
+
const selfSignUpFn = controller.slice(
|
|
143
|
+
controller.indexOf("async function addResidentSelfSignUp"),
|
|
144
|
+
controller.indexOf("async function add("),
|
|
145
|
+
);
|
|
146
|
+
assert.ok(selfSignUpFn.length > 0, "addResidentSelfSignUp was renamed or moved");
|
|
147
|
+
|
|
148
|
+
const dropAt = selfSignUpFn.indexOf("dropUnsentFiles(req.body)");
|
|
149
|
+
const validateAt = selfSignUpFn.indexOf("schemaResidentSelfSignUp.validate(");
|
|
150
|
+
assert.ok(dropAt > -1, "self-signup no longer drops unsent files");
|
|
151
|
+
assert.ok(validateAt > dropAt, "unsent files must be dropped BEFORE validation, or the 400 is unchanged");
|
|
152
|
+
assert.match(selfSignUpFn, /schemaResidentSelfSignUp\.validate\(\s*body\s*,/);
|
|
153
|
+
|
|
154
|
+
// Exactly one call site in the whole controller. If it ever appears on the
|
|
155
|
+
// authenticated `add`/`updateById` paths, staff stop being told about a
|
|
156
|
+
// client that is silently losing their attachments.
|
|
157
|
+
assert.equal(controller.match(/dropUnsentFiles\(/g)?.length, 1);
|
|
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);
|
|
276
|
+
});
|