@7365admin1/core 3.75.0 → 3.76.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/CHANGELOG.md +19 -0
- package/dist/index.d.ts +64 -3
- package/dist/index.js +150 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +148 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/camera-live-link.test.mjs +163 -0
- package/test/default-staff-role.test.mjs +1 -1
- package/test/e2e/client-invite-service365.e2e.test.mjs +57 -0
- package/test/service-app-type.test.mjs +35 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7365admin1/core",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.76.0",
|
|
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/anpr-plate.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/utils/camera-playback.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/org-modules.util.ts src/utils/site-modules.util.ts src/utils/resident-app-modules.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 src/utils/module-gate.util.ts src/utils/member-lookup.util.ts src/utils/notification-access.util.ts src/utils/module-access.util.ts src/services/module-gate.service.ts src/controllers/site.controller.ts src/controllers/organization.controller.ts src/controllers/member.controller.ts src/utils/org-owner-repair.util.ts src/utils/socket-session.util.ts src/utils/people-create-gate.util.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/anpr-plate.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/utils/camera-playback.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/org-modules.util.ts src/utils/site-modules.util.ts src/utils/resident-app-modules.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 src/utils/module-gate.util.ts src/utils/member-lookup.util.ts src/utils/notification-access.util.ts src/utils/module-access.util.ts src/services/module-gate.service.ts src/controllers/site.controller.ts src/controllers/organization.controller.ts src/controllers/member.controller.ts src/utils/org-owner-repair.util.ts src/utils/socket-session.util.ts src/utils/people-create-gate.util.ts src/utils/service-app-type.util.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
|
},
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server signs the LIVE view link, the same way it already signs recorded
|
|
3
|
+
* footage — and hands it out WITHOUT touching `camera.host`.
|
|
4
|
+
*
|
|
5
|
+
* Both halves are asserted here, and they are different claims:
|
|
6
|
+
*
|
|
7
|
+
* 1. `signLiveLink` produces exactly the string the relay's `playback.js`
|
|
8
|
+
* verifies (the expected signature is recomputed here by hand, not taken
|
|
9
|
+
* from the function under test).
|
|
10
|
+
* 2. The camera payloads this estate actually returns CARRY it — proved
|
|
11
|
+
* through `withLiveLink(publicCameraFields(...))`, the wrapper the wall,
|
|
12
|
+
* the health sweep and the single-camera view all go through.
|
|
13
|
+
*/
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { createHmac } from "node:crypto";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import test from "node:test";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
PLAYBACK_LINK_SECONDS,
|
|
22
|
+
signLiveLink,
|
|
23
|
+
signPlaybackLink,
|
|
24
|
+
withLiveLink,
|
|
25
|
+
} from "./.build/utils/camera-playback.util.mjs";
|
|
26
|
+
import {
|
|
27
|
+
isRelayPlayerUrl,
|
|
28
|
+
} from "./.build/utils/camera-capability.util.mjs";
|
|
29
|
+
import { publicCameraFields } from "./.build/utils/camera-view.util.mjs";
|
|
30
|
+
|
|
31
|
+
const KEY = "k".repeat(40);
|
|
32
|
+
const RELAY = "the-loft.example.net";
|
|
33
|
+
const RELAYS = { [RELAY]: KEY };
|
|
34
|
+
const NOW = new Date("2026-09-16T08:00:00Z");
|
|
35
|
+
const EXP = Math.floor(NOW.getTime() / 1000) + PLAYBACK_LINK_SECONDS;
|
|
36
|
+
|
|
37
|
+
/** What the relay computes. Written out here so the test is not the code twice. */
|
|
38
|
+
const expectedSig = (channel, exp = EXP, key = KEY) =>
|
|
39
|
+
createHmac("sha256", key).update(`${channel}|${exp}`).digest("hex");
|
|
40
|
+
|
|
41
|
+
test("a live link is signed over channel|exp with the relay's key", () => {
|
|
42
|
+
const link = signLiveLink(`https://${RELAY}/7`, { relays: RELAYS, now: NOW });
|
|
43
|
+
|
|
44
|
+
assert.equal(link.liveAuth, `exp=${EXP}&sig=${expectedSig("7")}`);
|
|
45
|
+
assert.equal(link.liveUrl, `https://${RELAY}/7?${link.liveAuth}`);
|
|
46
|
+
assert.equal(link.liveExpiresAt, new Date(EXP * 1000).toISOString());
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("the live link and the recorded-footage link sign DIFFERENT messages", () => {
|
|
50
|
+
const start = new Date("2026-09-16T07:00:00Z");
|
|
51
|
+
const end = new Date("2026-09-16T07:10:00Z");
|
|
52
|
+
const recorded = signPlaybackLink({
|
|
53
|
+
host: `https://${RELAY}/7`,
|
|
54
|
+
key: KEY,
|
|
55
|
+
start,
|
|
56
|
+
end,
|
|
57
|
+
now: NOW,
|
|
58
|
+
});
|
|
59
|
+
const live = signLiveLink(`https://${RELAY}/7`, { relays: RELAYS, now: NOW });
|
|
60
|
+
|
|
61
|
+
// Same key, same expiry, different signature - so a recording link cannot be
|
|
62
|
+
// replayed as a live link, which is what the relay's tests assert from the
|
|
63
|
+
// other side.
|
|
64
|
+
assert.equal(new URL(recorded.playerUrl).searchParams.get("exp"), String(EXP));
|
|
65
|
+
assert.notEqual(
|
|
66
|
+
new URL(recorded.playerUrl).searchParams.get("sig"),
|
|
67
|
+
new URL(live.liveUrl).searchParams.get("sig"),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("the link lives as long as a recorded-footage link, and no longer", () => {
|
|
72
|
+
assert.equal(PLAYBACK_LINK_SECONDS, 120);
|
|
73
|
+
const link = signLiveLink(`https://${RELAY}/7`, { relays: RELAYS, now: NOW });
|
|
74
|
+
const exp = Number(new URLSearchParams(link.liveAuth).get("exp"));
|
|
75
|
+
assert.equal(exp - Math.floor(NOW.getTime() / 1000), PLAYBACK_LINK_SECONDS);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("no relay configured for that address means NOTHING changes", () => {
|
|
79
|
+
// The estate has cameras on hosts nobody has configured. They must keep their
|
|
80
|
+
// plain link rather than get a signature no relay can check.
|
|
81
|
+
assert.equal(signLiveLink(`https://other.example.net/7`, { relays: RELAYS, now: NOW }), null);
|
|
82
|
+
assert.equal(signLiveLink(`https://${RELAY}/7`, { relays: {}, now: NOW }), null);
|
|
83
|
+
// A host with no channel is genuinely unresolvable, not channel 1.
|
|
84
|
+
assert.equal(signLiveLink(`https://${RELAY}/lobby`, { relays: RELAYS, now: NOW }), null);
|
|
85
|
+
assert.equal(signLiveLink(undefined, { relays: RELAYS, now: NOW }), null);
|
|
86
|
+
assert.equal(signLiveLink("", { relays: RELAYS, now: NOW }), null);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("one signature serves the page, the socket, the still and the clip", () => {
|
|
90
|
+
// It covers the CHANNEL, not the path - which is why appending it to
|
|
91
|
+
// /screenshot/<ch> and /record/<ch>/duration/<n> works, and why the clients
|
|
92
|
+
// that splice `screenshot` into the path are unaffected.
|
|
93
|
+
const { liveAuth } = signLiveLink(`https://${RELAY}/7`, { relays: RELAYS, now: NOW });
|
|
94
|
+
for (const path of ["/7", "/api/stream/7", "/screenshot/7", "/record/7/duration/10"]) {
|
|
95
|
+
const url = new URL(`https://${RELAY}${path}?${liveAuth}`);
|
|
96
|
+
assert.equal(url.searchParams.get("sig"), expectedSig("7"));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("the camera payloads the estate returns carry the signed link", () => {
|
|
101
|
+
const camera = { _id: "c1", name: "Lobby", type: "ip", status: "active", host: `https://${RELAY}/7` };
|
|
102
|
+
const fields = withLiveLink(publicCameraFields(camera), { relays: RELAYS, now: NOW });
|
|
103
|
+
|
|
104
|
+
assert.equal(fields.liveUrl, `https://${RELAY}/7?exp=${EXP}&sig=${expectedSig("7")}`);
|
|
105
|
+
assert.equal(fields.liveAuth, `exp=${EXP}&sig=${expectedSig("7")}`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("the stored host is handed back UNCHANGED, and stays a relay player URL", () => {
|
|
109
|
+
const host = `https://${RELAY}/7`;
|
|
110
|
+
const fields = withLiveLink(publicCameraFields({ _id: "c1", type: "ip", status: "active", host }), {
|
|
111
|
+
relays: RELAYS,
|
|
112
|
+
now: NOW,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// If `host` ever carried the signature, `isRelayPlayerUrl` would refuse it
|
|
116
|
+
// (it rejects any query), the liveVideo capability would switch off for both
|
|
117
|
+
// mobile apps and the web wall, and the patrol-route form would freeze an
|
|
118
|
+
// expiring signature into the route document. So this is load-bearing.
|
|
119
|
+
assert.equal(fields.host, host);
|
|
120
|
+
assert.equal(isRelayPlayerUrl(fields.host), true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The WIRING, asserted on the service source — the same reason and the same
|
|
125
|
+
* method as `camera-entitlement-wiring.test.mjs`: constructing the service needs
|
|
126
|
+
* a live Atlas connection, so the only way to prove the service actually calls
|
|
127
|
+
* this is to assert the source.
|
|
128
|
+
*
|
|
129
|
+
* Without this, every assertion above could pass while the wall still handed out
|
|
130
|
+
* unsigned links — a helper nobody called.
|
|
131
|
+
*/
|
|
132
|
+
test("EVERY camera payload the service returns goes through withLiveLink", () => {
|
|
133
|
+
const source = readFileSync(
|
|
134
|
+
fileURLToPath(new URL("../src/services/camera-view.service.ts", import.meta.url)),
|
|
135
|
+
"utf8",
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const uses = [...source.matchAll(/publicCameraFields\(/g)];
|
|
139
|
+
// The import line names it too; the call sites are what matter.
|
|
140
|
+
const callSites = uses.filter((m) => !/import|from/.test(source.slice(Math.max(0, m.index - 80), m.index)));
|
|
141
|
+
assert.ok(callSites.length >= 3, `expected the wall, the health sweep and the single view, found ${callSites.length}`);
|
|
142
|
+
|
|
143
|
+
for (const match of callSites) {
|
|
144
|
+
const before = source.slice(Math.max(0, match.index - 60), match.index);
|
|
145
|
+
assert.match(
|
|
146
|
+
before,
|
|
147
|
+
/withLiveLink\(\s*$/,
|
|
148
|
+
`a camera payload at offset ${match.index} is handed out without a signed live link`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("an ANPR camera gets no host and therefore no live link", () => {
|
|
154
|
+
// `publicCameraFields` already withholds `host` for ANPR; nothing may put one
|
|
155
|
+
// back. ANPR belongs to visitor and vehicle management, not to CCTV.
|
|
156
|
+
const fields = withLiveLink(
|
|
157
|
+
publicCameraFields({ _id: "c2", type: "anpr", status: "active", host: `https://${RELAY}/7` }),
|
|
158
|
+
{ relays: RELAYS, now: NOW },
|
|
159
|
+
);
|
|
160
|
+
assert.equal(fields.host, undefined);
|
|
161
|
+
assert.equal(fields.liveUrl, undefined);
|
|
162
|
+
assert.equal(fields.liveAuth, undefined);
|
|
163
|
+
});
|
|
@@ -127,7 +127,7 @@ test("getRoleByName applies the type to the query when one is given", () => {
|
|
|
127
127
|
);
|
|
128
128
|
assert.match(
|
|
129
129
|
fn,
|
|
130
|
-
/type \? \{ name, type \} : \{ name \}/,
|
|
130
|
+
/type \? \{ name, type: appTypeQuery\(type\) \} : \{ name \}/,
|
|
131
131
|
"getRoleByName no longer narrows by type when a type is supplied",
|
|
132
132
|
);
|
|
133
133
|
});
|
|
@@ -153,6 +153,63 @@ describe("Invite Client gives a Property Management client its Service365 module
|
|
|
153
153
|
const stored = await h.db.collection("roles").findOne({ org: org._id, name: "Estate lead" });
|
|
154
154
|
assert.equal(stored.type, "property_management_agency");
|
|
155
155
|
});
|
|
156
|
+
|
|
157
|
+
it("rows stored before 3.75.0 as real_estate_developer are still found, and still capped", async () => {
|
|
158
|
+
// Nobody has read production to prove no such row exists, so every lookup by
|
|
159
|
+
// type must find BOTH stored spellings.
|
|
160
|
+
const now = new Date().toISOString();
|
|
161
|
+
const hashed = (await h.db.collection("users").findOne({ email: RED_CLIENT })).password;
|
|
162
|
+
const oldEmail = "red-old-cis@e2e.example.com";
|
|
163
|
+
const newEmail = "red-new-cis@e2e.example.com";
|
|
164
|
+
const users = await h.db.collection("users").insertMany(
|
|
165
|
+
[oldEmail, newEmail].map((email) => ({ email, name: email.split("@")[0], status: "active", password: hashed, createdAt: now })),
|
|
166
|
+
);
|
|
167
|
+
const [oldUser, newUser] = [users.insertedIds[0], users.insertedIds[1]];
|
|
168
|
+
const org = new ObjectId();
|
|
169
|
+
const oldOwnerRole = new ObjectId();
|
|
170
|
+
await h.db.collection("roles").insertOne({
|
|
171
|
+
_id: oldOwnerRole, org, name: "owner", type: "real_estate_developer", default: true,
|
|
172
|
+
permissions: ["visitor-mgmt:view-visitors"], status: "active", createdAt: now, deletedAt: "",
|
|
173
|
+
});
|
|
174
|
+
await h.db.collection("organizations").insertOne({
|
|
175
|
+
_id: org, name: "CIS Old Developer", nature: "real_estate_developer", email: oldEmail,
|
|
176
|
+
invitedRole: oldOwnerRole, status: "active", createdAt: now, deletedAt: "",
|
|
177
|
+
});
|
|
178
|
+
await h.db.collection("members").insertMany([
|
|
179
|
+
{ user: oldUser, org, orgName: "CIS Old Developer", name: oldEmail, email: oldEmail, type: "organization", role: oldOwnerRole, siteId: "", status: "active", createdAt: now, deletedAt: "" },
|
|
180
|
+
{ user: oldUser, org, orgName: "CIS Old Developer", name: oldEmail, email: oldEmail, type: "real_estate_developer", role: oldOwnerRole, siteId: "", status: "active", createdAt: now, deletedAt: "" },
|
|
181
|
+
{ user: newUser, org, orgName: "CIS Old Developer", name: newEmail, email: newEmail, type: "property_management_agency", role: oldOwnerRole, siteId: "", status: "active", createdAt: now, deletedAt: "" },
|
|
182
|
+
]);
|
|
183
|
+
const oldSid = await h.login(oldEmail, PASSWORD);
|
|
184
|
+
const newSid = await h.login(newEmail, PASSWORD);
|
|
185
|
+
|
|
186
|
+
for (const [who, sidOf, stored] of [[oldUser, oldSid, "real_estate_developer"], [newUser, newSid, "property_management_agency"]]) {
|
|
187
|
+
for (const asked of ["property_management_agency", "real_estate_developer"]) {
|
|
188
|
+
const byOrg = await h.api(`/members/user/${who}/app/${asked}/org?org=${org}`, { sid: sidOf });
|
|
189
|
+
assert.equal(byOrg.status, 200, `${stored} asked as ${asked}: ${JSON.stringify(byOrg.body)}`);
|
|
190
|
+
assert.equal(byOrg.body.type, stored);
|
|
191
|
+
const plain = await h.api(`/members/user/${who}/app/${asked}`, { sid: sidOf });
|
|
192
|
+
assert.equal(plain.status, 200, `${stored} (no org) asked as ${asked}: ${JSON.stringify(plain.body)}`);
|
|
193
|
+
const modules = await h.api(`/members/user/${who}/app/${asked}/modules?org=${org}`, { sid: sidOf });
|
|
194
|
+
assert.equal(modules.status, 200, JSON.stringify(modules.body));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// The role list by type shows the old developer-typed owner role to the PM app.
|
|
199
|
+
const roles = await h.api(`/roles?org=${org}&type=property_management_agency`, { sid: oldSid });
|
|
200
|
+
assert.equal(roles.status, 200, JSON.stringify(roles.body));
|
|
201
|
+
const items = roles.body.items ?? roles.body;
|
|
202
|
+
assert.ok(items.some((r) => String(r._id) === String(oldOwnerRole)), "the stored developer role is listed");
|
|
203
|
+
|
|
204
|
+
// The owner grant on a developer-typed role still caps both type spellings.
|
|
205
|
+
for (const type of ["property_management_agency", "real_estate_developer"]) {
|
|
206
|
+
const wider = await h.api("/roles", {
|
|
207
|
+
method: "POST", sid: oldSid,
|
|
208
|
+
body: { name: `Old wide ${type}`, type, org: org.toString(), permissions: ["visitor-mgmt:delete-visitor"] },
|
|
209
|
+
});
|
|
210
|
+
assert.ok([401, 403].includes(wider.status), `${type}: got ${wider.status} ${JSON.stringify(wider.body)}`);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
156
213
|
});
|
|
157
214
|
|
|
158
215
|
async function seed(h) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Lookups by type must find every stored spelling of an application. Core 3.75.0
|
|
2
|
+
// writes a Real Estate Developer's memberships and roles as
|
|
3
|
+
// property_management_agency, but rows written before it carry
|
|
4
|
+
// real_estate_developer, and production has not been read to prove there are none.
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
|
|
8
|
+
import { appTypeQuery, sameAppType, serviceAppType, storedAppTypes } from "./.build/utils/service-app-type.util.mjs";
|
|
9
|
+
import { pickLookupMembership, pickSiteMembership } from "./.build/utils/member-lookup.util.mjs";
|
|
10
|
+
|
|
11
|
+
test("writes use the Property Management type; lookups match both stored values", () => {
|
|
12
|
+
assert.equal(serviceAppType("real_estate_developer"), "property_management_agency");
|
|
13
|
+
for (const asked of ["property_management_agency", "real_estate_developer"]) {
|
|
14
|
+
assert.deepEqual(storedAppTypes(asked).sort(), ["property_management_agency", "real_estate_developer"]);
|
|
15
|
+
assert.deepEqual(appTypeQuery(asked).$in.sort(), ["property_management_agency", "real_estate_developer"]);
|
|
16
|
+
}
|
|
17
|
+
assert.equal(appTypeQuery("security_agency"), "security_agency", "other apps keep an exact match");
|
|
18
|
+
assert.ok(sameAppType("real_estate_developer", "property_management_agency"));
|
|
19
|
+
assert.ok(!sameAppType("security_agency", "property_management_agency"));
|
|
20
|
+
assert.ok(!sameAppType("", ""));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("the module deny list picks a membership stored with either spelling", () => {
|
|
24
|
+
const org = "68b0f1a2c3d4e5f601234567";
|
|
25
|
+
const site = "68b0f1a2c3d4e5f601234568";
|
|
26
|
+
const old = { _id: "m1", type: "real_estate_developer", org, siteId: site, status: "active" };
|
|
27
|
+
const now = { _id: "m2", type: "property_management_agency", org, siteId: site, status: "active" };
|
|
28
|
+
for (const row of [old, now]) {
|
|
29
|
+
for (const asked of ["property_management_agency", "real_estate_developer"]) {
|
|
30
|
+
assert.equal(pickLookupMembership([row], asked, "", org)?._id, row._id, `${row.type} asked as ${asked}`);
|
|
31
|
+
assert.equal(pickSiteMembership([row], asked, site, org)?._id, row._id);
|
|
32
|
+
}
|
|
33
|
+
assert.equal(pickLookupMembership([row], "security_agency", "", org), null);
|
|
34
|
+
}
|
|
35
|
+
});
|