@reventlessdev/reventless-local 3.0.0-alpha.217 → 3.0.0-alpha.218
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 +12 -0
- package/package.json +11 -11
- package/src/BakedManifest.res +63 -51
- package/src/BakedManifest.res.mjs +44 -28
- package/src/HostShellDist.res +29 -0
- package/src/HostShellDist.res.mjs +20 -0
- package/src/Platform.res +30 -11
- package/src/Platform.res.mjs +6 -0
- package/src/ShellConfig.res +178 -0
- package/src/ShellConfig.res.mjs +114 -0
- package/src/UiHints.res +107 -0
- package/src/UiHints.res.mjs +61 -0
- package/src/adapter/Auth/LocalAuth.res +136 -6
- package/src/adapter/Auth/LocalAuth.res.mjs +82 -3
- package/src/adapter/DomainGraphQL_Server.res +67 -4
- package/src/adapter/DomainGraphQL_Server.res.mjs +49 -2
- package/src/adapter/GraphQL_Server.res.mjs +3 -0
- package/tests/BakedManifestFilesTest.res +68 -0
- package/tests/BakedManifestFilesTest.res.mjs +115 -0
- package/tests/ShellConfigTest.res +221 -0
- package/tests/ShellConfigTest.res.mjs +203 -0
- package/tests/UiHintsTest.res +94 -0
- package/tests/UiHintsTest.res.mjs +99 -0
- package/tests/adapter/LocalAuthLoginTest.res +214 -0
- package/tests/adapter/LocalAuthLoginTest.res.mjs +191 -8
- package/tests/adapter/LocalAuthUserStoreTest.res.mjs +2 -2
|
@@ -69,10 +69,11 @@ function handleLogin(req, res) {
|
|
|
69
69
|
if (parsed === undefined) {
|
|
70
70
|
return _loginRejected(res, "Invalid JSON body");
|
|
71
71
|
}
|
|
72
|
+
let activeRole = parsed.activeRole;
|
|
72
73
|
let username = parsed.username;
|
|
73
|
-
LocalAuth$ReventlessLocal.Login.issue(username, parsed.password).then(result => {
|
|
74
|
+
LocalAuth$ReventlessLocal.Login.issue(username, parsed.password, activeRole).then(result => {
|
|
74
75
|
if (result.TAG === "Ok") {
|
|
75
|
-
let i = LocalAuth$ReventlessLocal.
|
|
76
|
+
let i = LocalAuth$ReventlessLocal.Login.mintedIdentity(username, activeRole);
|
|
76
77
|
let identity = i !== undefined ? i : Identity$Reventless.anonymous;
|
|
77
78
|
let identityJson = S.reverseConvertToJsonOrThrow(identity, Identity$Reventless.schema);
|
|
78
79
|
_writeJson(res, 200, Object.fromEntries([
|
|
@@ -93,6 +94,48 @@ function handleLogin(req, res) {
|
|
|
93
94
|
});
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
function handleSwitchRole(req, res) {
|
|
98
|
+
readBody(req, body => {
|
|
99
|
+
let requested;
|
|
100
|
+
let exit = 0;
|
|
101
|
+
let json;
|
|
102
|
+
try {
|
|
103
|
+
json = JSON.parse(body);
|
|
104
|
+
exit = 1;
|
|
105
|
+
} catch (exn) {
|
|
106
|
+
requested = undefined;
|
|
107
|
+
}
|
|
108
|
+
if (exit === 1) {
|
|
109
|
+
requested = json.activeRole;
|
|
110
|
+
}
|
|
111
|
+
let presented = Stdlib_Option.flatMap(req.headers["authorization"], h => {
|
|
112
|
+
if (h.startsWith("Bearer ")) {
|
|
113
|
+
return h.slice(7, h.length).trim();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
if (presented === undefined) {
|
|
117
|
+
return _loginRejected(res, "Missing bearer token");
|
|
118
|
+
}
|
|
119
|
+
let msg = LocalAuth$ReventlessLocal.Login.reissue(presented, requested);
|
|
120
|
+
if (msg.TAG !== "Ok") {
|
|
121
|
+
return _loginRejected(res, msg._0);
|
|
122
|
+
}
|
|
123
|
+
let newToken = msg._0;
|
|
124
|
+
let i = LocalAuth$ReventlessLocal.Login.verifyAndDecode(newToken);
|
|
125
|
+
let identity = i !== undefined ? i : Identity$Reventless.anonymous;
|
|
126
|
+
_writeJson(res, 200, Object.fromEntries([
|
|
127
|
+
[
|
|
128
|
+
"token",
|
|
129
|
+
newToken
|
|
130
|
+
],
|
|
131
|
+
[
|
|
132
|
+
"identity",
|
|
133
|
+
S.reverseConvertToJsonOrThrow(identity, Identity$Reventless.schema)
|
|
134
|
+
]
|
|
135
|
+
]));
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
96
139
|
function handleLogout(_req, res) {
|
|
97
140
|
res.writeHead(204, {
|
|
98
141
|
"Access-Control-Allow-Origin": "*"
|
|
@@ -163,6 +206,9 @@ function _dispatch(req, res, yoga, getSdl) {
|
|
|
163
206
|
if (path === "/__inmemory/login" && req.method === "POST") {
|
|
164
207
|
return handleLogin(req, res);
|
|
165
208
|
}
|
|
209
|
+
if (path === "/__inmemory/switch-role" && req.method === "POST") {
|
|
210
|
+
return handleSwitchRole(req, res);
|
|
211
|
+
}
|
|
166
212
|
if (path === "/__inmemory/logout" && req.method === "POST") {
|
|
167
213
|
return handleLogout(req, res);
|
|
168
214
|
}
|
|
@@ -729,6 +775,7 @@ export {
|
|
|
729
775
|
_writeJson,
|
|
730
776
|
_loginRejected,
|
|
731
777
|
handleLogin,
|
|
778
|
+
handleSwitchRole,
|
|
732
779
|
handleLogout,
|
|
733
780
|
_corsWriteHeaders,
|
|
734
781
|
handleObjectPut,
|
|
@@ -16,6 +16,8 @@ let _loginRejected = DomainGraphQL_Server$ReventlessLocal._loginRejected;
|
|
|
16
16
|
|
|
17
17
|
let handleLogin = DomainGraphQL_Server$ReventlessLocal.handleLogin;
|
|
18
18
|
|
|
19
|
+
let handleSwitchRole = DomainGraphQL_Server$ReventlessLocal.handleSwitchRole;
|
|
20
|
+
|
|
19
21
|
let handleLogout = DomainGraphQL_Server$ReventlessLocal.handleLogout;
|
|
20
22
|
|
|
21
23
|
let _corsWriteHeaders = DomainGraphQL_Server$ReventlessLocal._corsWriteHeaders;
|
|
@@ -132,6 +134,7 @@ export {
|
|
|
132
134
|
_writeJson,
|
|
133
135
|
_loginRejected,
|
|
134
136
|
handleLogin,
|
|
137
|
+
handleSwitchRole,
|
|
135
138
|
handleLogout,
|
|
136
139
|
_corsWriteHeaders,
|
|
137
140
|
handleObjectPut,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// Which files a bake declaration produces, and under which names. The write
|
|
4
|
+
// itself needs a host-shell package on disk; this is the part that decides what
|
|
5
|
+
// gets written, so it is worth pinning without one.
|
|
6
|
+
|
|
7
|
+
let _ = TestRunner.setup()
|
|
8
|
+
|
|
9
|
+
let sel = (plugin): ReventlessInfra.Platform.bakedManifestSelection => {
|
|
10
|
+
plugin,
|
|
11
|
+
views: [],
|
|
12
|
+
commands: [],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let keysOf = (config: ReventlessInfra.Platform.bakedManifest) =>
|
|
16
|
+
BakedManifest.files(~config)->Array.map(((key, _)) => key)
|
|
17
|
+
|
|
18
|
+
describe("BakedManifest.files", () => {
|
|
19
|
+
open Expect
|
|
20
|
+
|
|
21
|
+
// The regression line: one declaration, one file, under the name every
|
|
22
|
+
// existing deployment's config.json already points at.
|
|
23
|
+
testSync("a declaration with no journeys produces exactly one file", () => {
|
|
24
|
+
expect(keysOf({components: [sel("Catalog")]}))->toEqual(["component-manifest.json"])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
testSync("a renamed default is honoured", () => {
|
|
28
|
+
expect(keysOf({components: [sel("Catalog")], key: "shop.json"}))->toEqual(["shop.json"])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// The default comes first and stays: it is what a caller matching no declared
|
|
32
|
+
// group gets, which locally includes the no-bearer identity every dev session
|
|
33
|
+
// starts from.
|
|
34
|
+
testSync("journeys are written beside the default, not instead of it", () => {
|
|
35
|
+
expect(
|
|
36
|
+
keysOf({
|
|
37
|
+
components: [sel("Catalog")],
|
|
38
|
+
journeys: [
|
|
39
|
+
{group: "Shopper", components: [sel("Catalog")]},
|
|
40
|
+
{group: "Fulfilment", components: [sel("Ordering")], key: "fulfil.json"},
|
|
41
|
+
],
|
|
42
|
+
}),
|
|
43
|
+
)->toEqual([
|
|
44
|
+
"component-manifest.json",
|
|
45
|
+
"component-manifest-shopper.json",
|
|
46
|
+
"fulfil.json",
|
|
47
|
+
])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// A group name is a Cognito identifier and a key is part of a URL, so the
|
|
51
|
+
// derivation folds anything that is neither a letter nor a digit.
|
|
52
|
+
testSync("derives a URL-safe key from an awkward group name", () => {
|
|
53
|
+
expect(
|
|
54
|
+
keysOf({components: [sel("Catalog")], journeys: [{group: "Ops Team/EU", components: []}]}),
|
|
55
|
+
)->toEqual(["component-manifest.json", "component-manifest-ops-team-eu.json"])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
testSync("carries each journey's own selections", () => {
|
|
59
|
+
let files = BakedManifest.files(~config={
|
|
60
|
+
components: [sel("Catalog")],
|
|
61
|
+
journeys: [{group: "Fulfilment", components: [sel("Ordering")]}],
|
|
62
|
+
})
|
|
63
|
+
expect(files->Array.map(((_, sels)) => sels->Array.map(s => s.plugin)))->toEqual([
|
|
64
|
+
["Catalog"],
|
|
65
|
+
["Ordering"],
|
|
66
|
+
])
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as TestRunner$ReventlessLocal from "../src/test/TestRunner.res.mjs";
|
|
4
|
+
import * as BakedManifest$ReventlessLocal from "../src/BakedManifest.res.mjs";
|
|
5
|
+
|
|
6
|
+
TestRunner$ReventlessLocal.setup();
|
|
7
|
+
|
|
8
|
+
function sel(plugin) {
|
|
9
|
+
return {
|
|
10
|
+
plugin: plugin,
|
|
11
|
+
views: [],
|
|
12
|
+
commands: []
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function keysOf(config) {
|
|
17
|
+
return BakedManifest$ReventlessLocal.files(config).map(param => param[0]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
globalThis.describe("BakedManifest.files", () => {
|
|
21
|
+
globalThis.test("a declaration with no journeys produces exactly one file", () => {
|
|
22
|
+
globalThis.expect(keysOf({
|
|
23
|
+
components: [{
|
|
24
|
+
plugin: "Catalog",
|
|
25
|
+
views: [],
|
|
26
|
+
commands: []
|
|
27
|
+
}]
|
|
28
|
+
})).toEqual(["component-manifest.json"]);
|
|
29
|
+
});
|
|
30
|
+
globalThis.test("a renamed default is honoured", () => {
|
|
31
|
+
globalThis.expect(keysOf({
|
|
32
|
+
components: [{
|
|
33
|
+
plugin: "Catalog",
|
|
34
|
+
views: [],
|
|
35
|
+
commands: []
|
|
36
|
+
}],
|
|
37
|
+
key: "shop.json"
|
|
38
|
+
})).toEqual(["shop.json"]);
|
|
39
|
+
});
|
|
40
|
+
globalThis.test("journeys are written beside the default, not instead of it", () => {
|
|
41
|
+
globalThis.expect(keysOf({
|
|
42
|
+
components: [{
|
|
43
|
+
plugin: "Catalog",
|
|
44
|
+
views: [],
|
|
45
|
+
commands: []
|
|
46
|
+
}],
|
|
47
|
+
journeys: [
|
|
48
|
+
{
|
|
49
|
+
group: "Shopper",
|
|
50
|
+
components: [{
|
|
51
|
+
plugin: "Catalog",
|
|
52
|
+
views: [],
|
|
53
|
+
commands: []
|
|
54
|
+
}]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
group: "Fulfilment",
|
|
58
|
+
components: [{
|
|
59
|
+
plugin: "Ordering",
|
|
60
|
+
views: [],
|
|
61
|
+
commands: []
|
|
62
|
+
}],
|
|
63
|
+
key: "fulfil.json"
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
})).toEqual([
|
|
67
|
+
"component-manifest.json",
|
|
68
|
+
"component-manifest-shopper.json",
|
|
69
|
+
"fulfil.json"
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
globalThis.test("derives a URL-safe key from an awkward group name", () => {
|
|
73
|
+
globalThis.expect(keysOf({
|
|
74
|
+
components: [{
|
|
75
|
+
plugin: "Catalog",
|
|
76
|
+
views: [],
|
|
77
|
+
commands: []
|
|
78
|
+
}],
|
|
79
|
+
journeys: [{
|
|
80
|
+
group: "Ops Team/EU",
|
|
81
|
+
components: []
|
|
82
|
+
}]
|
|
83
|
+
})).toEqual([
|
|
84
|
+
"component-manifest.json",
|
|
85
|
+
"component-manifest-ops-team-eu.json"
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
globalThis.test("carries each journey's own selections", () => {
|
|
89
|
+
let files = BakedManifest$ReventlessLocal.files({
|
|
90
|
+
components: [{
|
|
91
|
+
plugin: "Catalog",
|
|
92
|
+
views: [],
|
|
93
|
+
commands: []
|
|
94
|
+
}],
|
|
95
|
+
journeys: [{
|
|
96
|
+
group: "Fulfilment",
|
|
97
|
+
components: [{
|
|
98
|
+
plugin: "Ordering",
|
|
99
|
+
views: [],
|
|
100
|
+
commands: []
|
|
101
|
+
}]
|
|
102
|
+
}]
|
|
103
|
+
});
|
|
104
|
+
globalThis.expect(files.map(param => param[1].map(s => s.plugin))).toEqual([
|
|
105
|
+
["Catalog"],
|
|
106
|
+
["Ordering"]
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export {
|
|
112
|
+
sel,
|
|
113
|
+
keysOf,
|
|
114
|
+
}
|
|
115
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// The `config.json` overlay the local platform puts on the host shell's shipped
|
|
4
|
+
// file. Two things are under test and they fail differently: the key set (what a
|
|
5
|
+
// deployment's declaration turns into) and the baseline (what makes a second
|
|
6
|
+
// boot, and a *withdrawn* declaration, land where they should). Every failure
|
|
7
|
+
// here is silent at the point it happens and loud a long way away — a shell that
|
|
8
|
+
// boots with the wrong surface, or none.
|
|
9
|
+
|
|
10
|
+
let _ = TestRunner.setup()
|
|
11
|
+
|
|
12
|
+
let manifest = (~key: option<string>=?): ReventlessInfra.Platform.bakedManifest => {
|
|
13
|
+
components: [{plugin: "Catalog", views: ["Products"], commands: []}],
|
|
14
|
+
key: ?key,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let str = (d: dict<JSON.t>, k: string): option<string> =>
|
|
18
|
+
d->Dict.get(k)->Option.flatMap(JSON.Decode.string)
|
|
19
|
+
|
|
20
|
+
let threw = (f: unit => unit): bool =>
|
|
21
|
+
try {
|
|
22
|
+
f()
|
|
23
|
+
false
|
|
24
|
+
} catch {
|
|
25
|
+
| _ => true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("ShellConfig.overlay", () => {
|
|
29
|
+
testSync("is empty when the deployment declares nothing", () => {
|
|
30
|
+
let out = ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)
|
|
31
|
+
expect(out->Dict.keysToArray)->toEqual([])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
testSync("points manifestUrl at the bake's default key", () => {
|
|
35
|
+
let out = ShellConfig.overlay(~bakedManifest=Some(manifest()), ~shellConfig=None)
|
|
36
|
+
expect(out->str("manifestUrl"))->toEqual(Some("/component-manifest.json"))
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// The key and the URL are one string: the bake writes to the dist root, which
|
|
40
|
+
// is also the shell's URL root. A renamed file that kept the default URL would
|
|
41
|
+
// 404 on a shell that has no admin API to fall back to.
|
|
42
|
+
testSync("follows a renamed bake", () => {
|
|
43
|
+
let out = ShellConfig.overlay(
|
|
44
|
+
~bakedManifest=Some(manifest(~key="storefront.json")),
|
|
45
|
+
~shellConfig=None,
|
|
46
|
+
)
|
|
47
|
+
expect(out->str("manifestUrl"))->toEqual(Some("/storefront.json"))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
testSync("passes the deployment's own keys through verbatim", () => {
|
|
51
|
+
let out = ShellConfig.overlay(
|
|
52
|
+
~bakedManifest=None,
|
|
53
|
+
~shellConfig=Some(
|
|
54
|
+
Dict.fromArray([
|
|
55
|
+
("appName", JSON.Encode.string("Online Shop")),
|
|
56
|
+
("elevatedGroups", ["Admin"]->Array.map(JSON.Encode.string)->JSON.Encode.array),
|
|
57
|
+
]),
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
expect(out->str("appName"))->toEqual(Some("Online Shop"))
|
|
61
|
+
expect(out->Dict.get("elevatedGroups")->Option.isSome)->toBe(true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Silently resolving it either way points the shell at a manifest the platform
|
|
65
|
+
// did not write, with nothing in the diff to say so.
|
|
66
|
+
testSync("refuses a shellConfig key the platform computes", () =>
|
|
67
|
+
expect(
|
|
68
|
+
threw(() =>
|
|
69
|
+
ShellConfig.overlay(
|
|
70
|
+
~bakedManifest=Some(manifest()),
|
|
71
|
+
~shellConfig=Some(
|
|
72
|
+
Dict.fromArray([("manifestUrl", JSON.Encode.string("/elsewhere.json"))]),
|
|
73
|
+
),
|
|
74
|
+
)->ignore
|
|
75
|
+
),
|
|
76
|
+
)->toBe(true)
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe("ShellConfig.emit", () => {
|
|
81
|
+
let shipped = `{\n "apiEndpoint": "/graphql",\n "appName": "Shipped"\n}`
|
|
82
|
+
|
|
83
|
+
let distWithConfig = () => {
|
|
84
|
+
let dir = NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "reventless-shellconfig-"]))
|
|
85
|
+
NodeFs.writeFileSync(NodePath.join([dir, "config.json"]), shipped)
|
|
86
|
+
dir
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let readJson = (path: string): dict<JSON.t> =>
|
|
90
|
+
NodeFs.readFileSync(path)->JSON.parseOrThrow->JSON.Decode.object->Option.getOrThrow
|
|
91
|
+
|
|
92
|
+
let readConfig = (dir: string) => readJson(NodePath.join([dir, "config.json"]))
|
|
93
|
+
let readBaseline = (dir: string) => readJson(NodePath.join([dir, "config.base.json"]))
|
|
94
|
+
|
|
95
|
+
let named = name => Some(Dict.fromArray([("appName", JSON.Encode.string(name))]))
|
|
96
|
+
|
|
97
|
+
testSync("merges the overlay onto the shipped file and keeps its other keys", () => {
|
|
98
|
+
let dir = distWithConfig()
|
|
99
|
+
ShellConfig.emit(~bakedManifest=Some(manifest()), ~shellConfig=None, ~dir)
|
|
100
|
+
let out = readConfig(dir)
|
|
101
|
+
expect(out->str("manifestUrl"))->toEqual(Some("/component-manifest.json"))
|
|
102
|
+
expect(out->str("apiEndpoint"))->toEqual(Some("/graphql"))
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
testSync("leaves a platform that declares nothing entirely alone", () => {
|
|
106
|
+
let dir = distWithConfig()
|
|
107
|
+
ShellConfig.emit(~bakedManifest=None, ~shellConfig=None, ~dir)
|
|
108
|
+
expect(NodeFs.readFileSync(NodePath.join([dir, "config.json"])))->toEqual(shipped)
|
|
109
|
+
expect(NodeFs.existsSync(NodePath.join([dir, "config.base.json"])))->toBe(false)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// The whole reason a baseline is kept: boot 2 overlaying boot 1's output would
|
|
113
|
+
// read `appName: "First"` as shipped and carry it forever.
|
|
114
|
+
testSync("starts every boot from the shipped file, not the last output", () => {
|
|
115
|
+
let dir = distWithConfig()
|
|
116
|
+
ShellConfig.emit(~bakedManifest=None, ~shellConfig=named("First"), ~dir)
|
|
117
|
+
ShellConfig.emit(~bakedManifest=None, ~shellConfig=named("Second"), ~dir)
|
|
118
|
+
expect(readConfig(dir)->str("appName"))->toEqual(Some("Second"))
|
|
119
|
+
expect(readBaseline(dir)->str("appName"))->toEqual(Some("Shipped"))
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Withdrawing a declaration has to be as reachable as making it: a lingering
|
|
123
|
+
// manifestUrl points the shell at a file nothing writes any more, and the
|
|
124
|
+
// symptom is an empty shop.
|
|
125
|
+
testSync("restores the shipped file once the declaration is withdrawn", () => {
|
|
126
|
+
let dir = distWithConfig()
|
|
127
|
+
ShellConfig.emit(~bakedManifest=Some(manifest()), ~shellConfig=None, ~dir)
|
|
128
|
+
ShellConfig.emit(~bakedManifest=None, ~shellConfig=None, ~dir)
|
|
129
|
+
let out = readConfig(dir)
|
|
130
|
+
expect(out->Dict.get("manifestUrl"))->toEqual(None)
|
|
131
|
+
expect(out->str("appName"))->toEqual(Some("Shipped"))
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Writing only the declared keys would boot a shell with no apiEndpoint, and
|
|
135
|
+
// that failure surfaces nowhere near here.
|
|
136
|
+
testSync("refuses to overlay a dist that ships no config.json", () => {
|
|
137
|
+
let dir = NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "reventless-shellconfig-"]))
|
|
138
|
+
expect(
|
|
139
|
+
threw(() => ShellConfig.emit(~bakedManifest=Some(manifest()), ~shellConfig=None, ~dir)),
|
|
140
|
+
)->toBe(true)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// ── Journeys ──────────────────────────────────────────────────────────────
|
|
145
|
+
//
|
|
146
|
+
// One curated surface per audience, beside the default one. The property under
|
|
147
|
+
// test throughout is that a deployment declaring none is untouched: journeys are
|
|
148
|
+
// what a shop with several audiences opts into, and every existing deployment
|
|
149
|
+
// has exactly one.
|
|
150
|
+
|
|
151
|
+
let withJourneys = (
|
|
152
|
+
~journeys: array<ReventlessInfra.Platform.bakedJourney>,
|
|
153
|
+
): ReventlessInfra.Platform.bakedManifest => {
|
|
154
|
+
components: [{plugin: "Catalog", views: ["Products"], commands: []}],
|
|
155
|
+
journeys,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let shopper: ReventlessInfra.Platform.bakedJourney = {
|
|
159
|
+
group: "Shopper",
|
|
160
|
+
components: [{plugin: "Catalog", views: ["Products"], commands: []}],
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let fulfilment: ReventlessInfra.Platform.bakedJourney = {
|
|
164
|
+
group: "Fulfilment",
|
|
165
|
+
components: [{plugin: "Ordering", views: ["Orders"], commands: ["ShipOrder"]}],
|
|
166
|
+
key: "fulfilment.json",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
describe("ShellConfig.overlay — journeys", () => {
|
|
170
|
+
open Expect
|
|
171
|
+
|
|
172
|
+
// The regression line. A shell that has never heard of journeys must not
|
|
173
|
+
// suddenly find a key it does not know.
|
|
174
|
+
testSync("a bake declaring no journeys writes no map", () => {
|
|
175
|
+
let out = ShellConfig.overlay(~bakedManifest=Some(manifest()), ~shellConfig=None)
|
|
176
|
+
expect(out->Dict.get("journeyManifestUrls"))->toEqual(None)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
testSync("an empty journeys array is the same as none", () => {
|
|
180
|
+
let out = ShellConfig.overlay(~bakedManifest=Some(withJourneys(~journeys=[])), ~shellConfig=None)
|
|
181
|
+
expect(out->Dict.get("journeyManifestUrls"))->toEqual(None)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// The default journey keeps `manifestUrl`, so a caller matching no declared
|
|
185
|
+
// group lands where every caller landed before.
|
|
186
|
+
testSync("keeps manifestUrl as the default journey", () => {
|
|
187
|
+
let out = ShellConfig.overlay(
|
|
188
|
+
~bakedManifest=Some(withJourneys(~journeys=[shopper])),
|
|
189
|
+
~shellConfig=None,
|
|
190
|
+
)
|
|
191
|
+
expect(out->str("manifestUrl"))->toEqual(Some("/component-manifest.json"))
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
testSync("maps each declared group to its own file", () => {
|
|
195
|
+
let out = ShellConfig.overlay(
|
|
196
|
+
~bakedManifest=Some(withJourneys(~journeys=[shopper, fulfilment])),
|
|
197
|
+
~shellConfig=None,
|
|
198
|
+
)
|
|
199
|
+
let map =
|
|
200
|
+
out->Dict.get("journeyManifestUrls")->Option.flatMap(JSON.Decode.object)->Option.getOrThrow
|
|
201
|
+
expect((map->str("Shopper"), map->str("Fulfilment")))->toEqual((
|
|
202
|
+
// Derived from the group, lower-cased, because a key is part of a URL.
|
|
203
|
+
Some("/component-manifest-shopper.json"),
|
|
204
|
+
// Named explicitly, and the declaration wins.
|
|
205
|
+
Some("/fulfilment.json"),
|
|
206
|
+
))
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// A passthrough cannot redirect a key the platform computes — the same rule
|
|
210
|
+
// `manifestUrl` already carries, extended to the map it now writes beside it.
|
|
211
|
+
testSync("refuses a shellConfig that sets the journey map itself", () =>
|
|
212
|
+
expect(
|
|
213
|
+
threw(() =>
|
|
214
|
+
ShellConfig.overlay(
|
|
215
|
+
~bakedManifest=Some(withJourneys(~journeys=[shopper])),
|
|
216
|
+
~shellConfig=Some(Dict.fromArray([("journeyManifestUrls", JSON.Encode.object(Dict.make()))])),
|
|
217
|
+
)->ignore
|
|
218
|
+
),
|
|
219
|
+
)->toBe(true)
|
|
220
|
+
)
|
|
221
|
+
})
|