@7365admin1/core 3.52.11 → 3.52.12
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 +18 -0
- package/dist/index.d.ts +9 -6
- package/dist/index.js +64 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +64 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/building-estate-site-scope.test.mjs +157 -0
package/package.json
CHANGED
|
@@ -0,0 +1,157 @@
|
|
|
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 { entitledSiteScope } from "./.build/utils/camera-view.util.mjs";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The fourth instance of the `/api/vehicles` defect, and the last of the estate
|
|
10
|
+
* reads: `GET /api/buildings`, `GET /api/building-units` and
|
|
11
|
+
* `GET /api/building-levels` all took an OPTIONAL `site`, applied it as a plain
|
|
12
|
+
* filter (`...(site && { site })`) and checked nothing, so a request that named
|
|
13
|
+
* no site was answered from the WHOLE collection — every client's blocks, units
|
|
14
|
+
* and levels. `GET /api/building-units/site/:site` took the site from the URL
|
|
15
|
+
* and never authorised it either, and that one returns units WITH their owners.
|
|
16
|
+
*
|
|
17
|
+
* Measured read-only on staging, 2026-09-03: `buildings` 125 rows, 0 with no
|
|
18
|
+
* site; `building-units` 1386 rows, 0 with no site; `building-levels` 401 rows,
|
|
19
|
+
* 0 with no site. A `$in` predicate therefore drops nothing that a legitimate
|
|
20
|
+
* caller can see today.
|
|
21
|
+
*
|
|
22
|
+
* The DECISION is `entitledSiteScope`, asked here directly. The WIRING needs a
|
|
23
|
+
* live Atlas connection to exercise, so it is asserted on the source exactly as
|
|
24
|
+
* `vehicle-site-scope.test.mjs` and `visitor-document-site-scope.test.mjs` do.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const ORG_A = "aaaaaaaaaaaaaaaaaaaaaaaa";
|
|
28
|
+
const ORG_B = "bbbbbbbbbbbbbbbbbbbbbbbb";
|
|
29
|
+
const SITE_A1 = "a1a1a1a1a1a1a1a1a1a1a1a1";
|
|
30
|
+
const SITE_A2 = "a2a2a2a2a2a2a2a2a2a2a2a2";
|
|
31
|
+
const SITE_B1 = "b1b1b1b1b1b1b1b1b1b1b1b1";
|
|
32
|
+
|
|
33
|
+
const sitesByOrg = { [ORG_A]: [SITE_A1, SITE_A2], [ORG_B]: [SITE_B1] };
|
|
34
|
+
|
|
35
|
+
const read = (p) =>
|
|
36
|
+
readFileSync(fileURLToPath(new URL(p, import.meta.url)), "utf8");
|
|
37
|
+
|
|
38
|
+
const controllers = {
|
|
39
|
+
building: read("../src/controllers/building.controller.ts"),
|
|
40
|
+
"building-unit": read("../src/controllers/building-unit.controller.ts"),
|
|
41
|
+
"building-level": read("../src/controllers/building-level.controller.ts"),
|
|
42
|
+
};
|
|
43
|
+
const repositories = {
|
|
44
|
+
building: read("../src/repositories/building.repository.ts"),
|
|
45
|
+
"building-unit": read("../src/repositories/building-unit.repository.ts"),
|
|
46
|
+
"building-level": read("../src/repositories/building-level.repository.ts"),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
test("no site + a site-level role sees its own site only", () => {
|
|
50
|
+
const scope = entitledSiteScope({
|
|
51
|
+
memberships: [{ org: ORG_A, siteId: SITE_A1, orgLevelRole: false }],
|
|
52
|
+
sitesByOrg,
|
|
53
|
+
engagedSitesByOrg: {},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
assert.deepEqual(scope, [SITE_A1]);
|
|
57
|
+
assert.ok(!scope.includes(SITE_A2), "must not reach a sibling site");
|
|
58
|
+
assert.ok(!scope.includes(SITE_B1), "must never reach another organisation");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("no site + an org-level role sees its whole org and nothing from another", () => {
|
|
62
|
+
const scope = entitledSiteScope({
|
|
63
|
+
memberships: [{ org: ORG_A, siteId: SITE_A1, orgLevelRole: true }],
|
|
64
|
+
sitesByOrg,
|
|
65
|
+
engagedSitesByOrg: {},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
assert.deepEqual([...scope].sort(), [SITE_A1, SITE_A2].sort());
|
|
69
|
+
assert.ok(!scope.includes(SITE_B1), "another organisation is never in scope");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a caller with no live membership gets an empty scope, not the estate", () => {
|
|
73
|
+
const scope = entitledSiteScope({
|
|
74
|
+
memberships: [],
|
|
75
|
+
sitesByOrg,
|
|
76
|
+
engagedSitesByOrg: {},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
assert.deepEqual(scope, []);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
for (const [name, source] of Object.entries(controllers)) {
|
|
83
|
+
test(`${name}.getAll resolves a scope when no site is named`, () => {
|
|
84
|
+
assert.equal(
|
|
85
|
+
source.match(/const siteScope = site \? null : await entitledSites\(req\);/g)
|
|
86
|
+
?.length,
|
|
87
|
+
1,
|
|
88
|
+
"exactly one list handler resolves the fallback scope",
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// An explicit site is AUTHORISED, not trusted: a site the caller cannot
|
|
92
|
+
// reach answers as a site that does not exist.
|
|
93
|
+
assert.ok(
|
|
94
|
+
/if \(site\) await requireSiteReach\(req, site\);/.test(source),
|
|
95
|
+
"an explicit site must be authorised",
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
assert.ok(
|
|
99
|
+
/^ +siteScope,\s*$/m.test(source),
|
|
100
|
+
"the scope must reach the repository, not be resolved and dropped",
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
assert.ok(
|
|
104
|
+
/import \{ entitledSites, requireSiteReach \} from "\.\.\/utils\/site-reach\.util";/.test(
|
|
105
|
+
source,
|
|
106
|
+
),
|
|
107
|
+
"the rule is the shared one, not a fourth implementation",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const [name, source] of Object.entries(repositories)) {
|
|
113
|
+
test(`${name} repository applies an empty scope rather than dropping it`, () => {
|
|
114
|
+
assert.equal(
|
|
115
|
+
source.match(/^ +if \(siteScope\) \{$/gm)?.length,
|
|
116
|
+
1,
|
|
117
|
+
"exactly one getAll applies the restriction",
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// `siteScope?.length` here would turn "reaches no site" back into "sees
|
|
121
|
+
// everything", which is the whole defect.
|
|
122
|
+
assert.ok(
|
|
123
|
+
!/siteScope\?\.length|siteScope && siteScope\.length/.test(source),
|
|
124
|
+
"an empty array must not collapse into the no-restriction branch",
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// The restriction is part of the CACHE KEY too. These three lists are all
|
|
128
|
+
// Redis-cached on the query alone, so without this the first caller's page
|
|
129
|
+
// is served to the second.
|
|
130
|
+
assert.ok(
|
|
131
|
+
/cacheParams\.siteScope = scoped\.length/.test(source),
|
|
132
|
+
"the scope must be part of the cache key",
|
|
133
|
+
);
|
|
134
|
+
assert.ok(
|
|
135
|
+
/"no-site"/.test(source),
|
|
136
|
+
"an empty scope needs a key that cannot collide with no restriction",
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
test("the owner-bearing unit read authorises the site in its URL", () => {
|
|
142
|
+
const source = controllers["building-unit"];
|
|
143
|
+
const handler = source.slice(
|
|
144
|
+
source.indexOf("async function getBuildingUnitsWithOwner("),
|
|
145
|
+
source.indexOf("async function getAllLevelsWithUnits("),
|
|
146
|
+
);
|
|
147
|
+
assert.ok(handler.length > 0, "the handler must still be there");
|
|
148
|
+
assert.ok(
|
|
149
|
+
/await requireSiteReach\(req, site\);/.test(handler),
|
|
150
|
+
"the site comes from the URL and must be checked",
|
|
151
|
+
);
|
|
152
|
+
assert.ok(
|
|
153
|
+
handler.indexOf("requireSiteReach") <
|
|
154
|
+
handler.indexOf("_getBuildingUnitsWithOwner("),
|
|
155
|
+
"the check must run BEFORE the read, not after it",
|
|
156
|
+
);
|
|
157
|
+
});
|