@aerokit/sdk 14.1.1 → 14.2.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 +1 -1
- package/test/flows/my.js +79 -0
- package/test/index.js +2 -0
package/package.json
CHANGED
package/test/flows/my.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { makeApi } from '../api.js';
|
|
2
|
+
import { expect, test } from '../fixtures.js';
|
|
3
|
+
import { resolveRelationSamples } from '../form.js';
|
|
4
|
+
import { sampleRecord } from '../sample-values.js';
|
|
5
|
+
|
|
6
|
+
// The personal (my) surface WIRE contract, driven when the manifest marks an entity `personal`:
|
|
7
|
+
// the scoped <Entity>MyController filters reads to the identity-mapped user, forces the owner FK
|
|
8
|
+
// server-side on create (whatever the client sends is ignored), strips every `sensitive` field
|
|
9
|
+
// from its responses (the allow-list is the security boundary - UI hiding alone is cosmetic),
|
|
10
|
+
// serves foreign rows as 404, and deletes only own rows.
|
|
11
|
+
//
|
|
12
|
+
// Prerequisite: the DEV IDENTITY mapping - a row of the owner relation's target whose identity
|
|
13
|
+
// field equals the test user (e.g. an Employee with email 'admin'). Without it the personal
|
|
14
|
+
// surface is EMPTY by design (never an error), so the flow SKIPS with a pointer instead of
|
|
15
|
+
// failing. The personal UI parity assertions (resolved labels, chat layout, calendar views on
|
|
16
|
+
// the My shell) are deliberately NOT asserted yet - they track the platform's personal-template
|
|
17
|
+
// parity fixes; this flow pins down the wire contract those pages consume.
|
|
18
|
+
export function myFlow(manifest, entity, opts = {}) {
|
|
19
|
+
const cfg = opts.extend?.entities?.[entity.name] ?? {};
|
|
20
|
+
if (!entity.personal || new Set(cfg.skip ?? []).has('my')) return;
|
|
21
|
+
const idProperty = manifest.idProperty ?? 'Id';
|
|
22
|
+
// the same entity through the scoped controller
|
|
23
|
+
const mine = { ...entity, api: entity.personal.api };
|
|
24
|
+
|
|
25
|
+
async function buildPayload(api) {
|
|
26
|
+
const payload = sampleRecord(entity);
|
|
27
|
+
for (const sample of await resolveRelationSamples(api, manifest, entity)) {
|
|
28
|
+
payload[sample.relation.name] = sample.id;
|
|
29
|
+
}
|
|
30
|
+
// the owner FK is server-forced on the personal surface; sending a value must be pointless
|
|
31
|
+
delete payload[entity.personal.owner];
|
|
32
|
+
return payload;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test(`${entity.name}: personal (my) surface scopes rows, forces the owner and strips sensitive fields`, async ({ api }) => {
|
|
36
|
+
const client = makeApi(api, manifest);
|
|
37
|
+
|
|
38
|
+
const createdResponse = await api.post(manifest.restBase + mine.api, { data: await buildPayload(api) });
|
|
39
|
+
test.skip(
|
|
40
|
+
!createdResponse.ok(),
|
|
41
|
+
`personal create returned ${createdResponse.status()} - the test user has no identity mapping; ` +
|
|
42
|
+
`seed the dev identity row (an owner-target record whose identity field equals the login user)`
|
|
43
|
+
);
|
|
44
|
+
const created = JSON.parse(await createdResponse.text());
|
|
45
|
+
const id = created?.[idProperty];
|
|
46
|
+
expect(id, 'personal create carries the generated id').toBeTruthy();
|
|
47
|
+
let foreignId;
|
|
48
|
+
try {
|
|
49
|
+
const own = await client.get(mine, id);
|
|
50
|
+
expect(own[entity.personal.owner], 'the owner FK is forced server-side on a personal create').toBeTruthy();
|
|
51
|
+
for (const field of entity.personal.sensitive ?? []) {
|
|
52
|
+
expect(own[field], `sensitive field ${field} must never reach the personal wire`).toBeFalsy();
|
|
53
|
+
}
|
|
54
|
+
const rows = await client.list(mine, 1000);
|
|
55
|
+
expect(rows.some((row) => row[idProperty] === id), 'the own row appears in the personal list').toBe(true);
|
|
56
|
+
|
|
57
|
+
// the power surface still serves the full record
|
|
58
|
+
const power = await client.get(entity, id);
|
|
59
|
+
expect(power[idProperty]).toBe(id);
|
|
60
|
+
|
|
61
|
+
// a row with no owner is foreign to everyone: the personal controller must 404 it and the
|
|
62
|
+
// personal list must not include it (only checkable when the owner relation is optional)
|
|
63
|
+
const ownerRelation = (entity.relations ?? []).find((relation) => relation.name === entity.personal.owner);
|
|
64
|
+
if (ownerRelation && !ownerRelation.required) {
|
|
65
|
+
const foreign = await client.create(entity, await buildPayload(api));
|
|
66
|
+
foreignId = foreign?.[idProperty];
|
|
67
|
+
const foreignThroughMine = await api.get(manifest.restBase + mine.api + '/' + foreignId);
|
|
68
|
+
expect(foreignThroughMine.status(), 'a foreign row must 404 through the personal controller').toBe(404);
|
|
69
|
+
const rowsAfter = await client.list(mine, 1000);
|
|
70
|
+
expect(rowsAfter.some((row) => row[idProperty] === foreignId), 'a foreign row must not appear in the personal list').toBe(false);
|
|
71
|
+
}
|
|
72
|
+
} finally {
|
|
73
|
+
await client.remove(mine, id); // deleting the OWN row through the personal controller works
|
|
74
|
+
if (foreignId) await client.remove(entity, foreignId);
|
|
75
|
+
}
|
|
76
|
+
const gone = await api.get(manifest.restBase + mine.api + '/' + id);
|
|
77
|
+
expect(gone.status()).toBe(404);
|
|
78
|
+
});
|
|
79
|
+
}
|
package/test/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { test } from './fixtures.js';
|
|
|
3
3
|
import { crudFlow } from './flows/crud.js';
|
|
4
4
|
import { listFlow } from './flows/list.js';
|
|
5
5
|
import { multilingualFlow } from './flows/multilingual.js';
|
|
6
|
+
import { myFlow } from './flows/my.js';
|
|
6
7
|
import { restFlow } from './flows/rest.js';
|
|
7
8
|
import { shellFlow } from './flows/shell.js';
|
|
8
9
|
|
|
@@ -16,6 +17,7 @@ export function runTest(manifestRef, opts = {}) {
|
|
|
16
17
|
listFlow(manifest, entity, opts);
|
|
17
18
|
crudFlow(manifest, entity, opts);
|
|
18
19
|
restFlow(manifest, entity, opts);
|
|
20
|
+
myFlow(manifest, entity, opts);
|
|
19
21
|
multilingualFlow(manifest, entity, opts);
|
|
20
22
|
shellFlow(manifest, entity, opts);
|
|
21
23
|
});
|