@reventlessdev/reventless-seed 1.0.0-alpha.2 → 1.0.0-alpha.21
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 +149 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/.sourcedirs.json +1 -0
- package/lib/bs/ReventlessSeed.cmi +0 -0
- package/lib/bs/ReventlessSeed.cmt +0 -0
- package/lib/bs/ReventlessSeed.mlmap +6 -0
- package/lib/bs/compiler-info.json +2 -2
- package/lib/bs/src/Seed-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed.ast +0 -0
- package/lib/bs/src/Seed.res +12 -3
- package/lib/bs/src/Seed.res.mjs +3 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Client.ast +0 -0
- package/lib/bs/src/Seed_Client.res +382 -21
- package/lib/bs/src/Seed_Client.res.mjs +332 -24
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Connect.ast +0 -0
- package/lib/bs/src/Seed_Connect.res +281 -33
- package/lib/bs/src/Seed_Connect.res.mjs +162 -8
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Prompt.ast +0 -0
- package/lib/bs/src/Seed_Prompt.res +140 -41
- package/lib/bs/src/Seed_Prompt.res.mjs +88 -13
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Random.ast +0 -0
- package/lib/bs/src/Seed_Random.res +4 -2
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Runner.ast +0 -0
- package/lib/bs/src/Seed_Runner.res +45 -19
- package/lib/bs/src/Seed_Runner.res.mjs +19 -1
- package/lib/bs/src/Seed_Types-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Upload.ast +0 -0
- package/lib/bs/src/Seed_Upload.res +43 -43
- package/lib/bs/src/Seed_Upload.res.mjs +65 -69
- package/lib/bs/src/Seed_Users-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Users-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Users-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Users.ast +0 -0
- package/lib/bs/src/Seed_Users.res +92 -0
- package/lib/bs/src/Seed_Users.res.mjs +91 -0
- package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_ClientTest.ast +0 -0
- package/lib/bs/tests/Seed_ClientTest.res +287 -0
- package/lib/bs/tests/Seed_ClientTest.res.mjs +268 -0
- package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_ConnectTest.ast +0 -0
- package/lib/bs/tests/Seed_ConnectTest.res +64 -0
- package/lib/bs/tests/Seed_ConnectTest.res.mjs +56 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_PromptTest.ast +0 -0
- package/lib/bs/tests/Seed_PromptTest.res +55 -0
- package/lib/bs/tests/Seed_PromptTest.res.mjs +63 -0
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_RandomTest.ast +0 -0
- package/lib/bs/tests/Seed_RandomTest.res +200 -0
- package/lib/bs/tests/Seed_RandomTest.res.mjs +208 -0
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_UsersTest.ast +0 -0
- package/lib/bs/tests/Seed_UsersTest.res +129 -0
- package/lib/bs/tests/Seed_UsersTest.res.mjs +113 -0
- package/lib/ocaml/.compiler.log +2 -2
- package/lib/ocaml/ReventlessSeed.cmi +0 -0
- package/lib/ocaml/ReventlessSeed.cmt +0 -0
- package/lib/ocaml/ReventlessSeed.mlmap +6 -0
- package/lib/ocaml/Seed-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed.ast +0 -0
- package/lib/ocaml/Seed.res +12 -3
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Client.ast +0 -0
- package/lib/ocaml/Seed_Client.res +382 -21
- package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_ClientTest.ast +0 -0
- package/lib/ocaml/Seed_ClientTest.res +287 -0
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Connect.ast +0 -0
- package/lib/ocaml/Seed_Connect.res +281 -33
- package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_ConnectTest.ast +0 -0
- package/lib/ocaml/Seed_ConnectTest.res +64 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Prompt.ast +0 -0
- package/lib/ocaml/Seed_Prompt.res +140 -41
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_PromptTest.ast +0 -0
- package/lib/ocaml/Seed_PromptTest.res +55 -0
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Random.ast +0 -0
- package/lib/ocaml/Seed_Random.res +4 -2
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_RandomTest.ast +0 -0
- package/lib/ocaml/Seed_RandomTest.res +200 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Runner.ast +0 -0
- package/lib/ocaml/Seed_Runner.res +45 -19
- package/lib/ocaml/Seed_Types-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Upload.ast +0 -0
- package/lib/ocaml/Seed_Upload.res +43 -43
- package/lib/ocaml/Seed_Users-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Users-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Users-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Users.ast +0 -0
- package/lib/ocaml/Seed_Users.res +92 -0
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_UsersTest.ast +0 -0
- package/lib/ocaml/Seed_UsersTest.res +129 -0
- package/package.json +7 -2
- package/rescript.json +9 -1
- package/src/Seed.res +12 -3
- package/src/Seed.res.mjs +3 -0
- package/src/Seed_Client.res +382 -21
- package/src/Seed_Client.res.mjs +332 -24
- package/src/Seed_Connect.res +281 -33
- package/src/Seed_Connect.res.mjs +162 -8
- package/src/Seed_Prompt.res +140 -41
- package/src/Seed_Prompt.res.mjs +88 -13
- package/src/Seed_Random.res +4 -2
- package/src/Seed_Runner.res +45 -19
- package/src/Seed_Runner.res.mjs +19 -1
- package/src/Seed_Upload.res +43 -43
- package/src/Seed_Upload.res.mjs +65 -69
- package/src/Seed_Users.res +92 -0
- package/src/Seed_Users.res.mjs +91 -0
- package/tests/Seed_ClientTest.res +287 -0
- package/tests/Seed_ClientTest.res.mjs +268 -0
- package/tests/Seed_ConnectTest.res +64 -0
- package/tests/Seed_ConnectTest.res.mjs +56 -0
- package/tests/Seed_PromptTest.res +55 -0
- package/tests/Seed_PromptTest.res.mjs +63 -0
- package/tests/Seed_RandomTest.res +200 -0
- package/tests/Seed_RandomTest.res.mjs +208 -0
- package/tests/Seed_UsersTest.res +129 -0
- package/tests/Seed_UsersTest.res.mjs +113 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// A seed issues hundreds of commands against runtimes a preceding reset left
|
|
4
|
+
// cold, so the endpoint faulting on one of them is a normal event rather than an
|
|
5
|
+
// exceptional one. Before the retry, a single `InternalFailure` in that batch
|
|
6
|
+
// aborted the run and left a half-seeded store that could only be recovered by a
|
|
7
|
+
// full wipe and a re-run — for a request that would have succeeded on being
|
|
8
|
+
// asked again.
|
|
9
|
+
//
|
|
10
|
+
// What makes the retry safe is the classifier, so that is what these pin. A
|
|
11
|
+
// fault on the endpoint's side carries no information about the document and is
|
|
12
|
+
// worth resending; anything that is an *answer* — a validation error, an unknown
|
|
13
|
+
// field, a refused token — comes back identical however many times it is asked,
|
|
14
|
+
// and resending it only delays the failure the operator needs to see.
|
|
15
|
+
|
|
16
|
+
let errors = entries => JSON.Encode.array(entries->Array.map(JSON.Encode.object))
|
|
17
|
+
|
|
18
|
+
let errorOfType = errorType => Dict.fromArray([("errorType", JSON.Encode.string(errorType))])
|
|
19
|
+
|
|
20
|
+
describe("Seed_Client.isTransient:", () => {
|
|
21
|
+
testSync("resends a fault on the endpoint's own side", () =>
|
|
22
|
+
[
|
|
23
|
+
"InternalFailure",
|
|
24
|
+
"ServiceUnavailable",
|
|
25
|
+
"Throttling",
|
|
26
|
+
"TooManyRequestsException",
|
|
27
|
+
]->Array.forEach(t => expect(Seed_Client.isTransient(errors([errorOfType(t)])))->toBe(true))
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
// The exact payload the seed aborted on.
|
|
31
|
+
testSync("resends the shape AppSync actually returned", () => {
|
|
32
|
+
let payload = JSON.parseOrThrow(`[{"errorType":"InternalFailure","message":"An internal failure occurred."}]`)
|
|
33
|
+
expect(Seed_Client.isTransient(payload))->toBe(true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
testSync("fails fast on an error that is an answer", () =>
|
|
37
|
+
["ValidationException", "Unauthorized", "FieldUndefined"]->Array.forEach(
|
|
38
|
+
t => expect(Seed_Client.isTransient(errors([errorOfType(t)])))->toBe(false),
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// A real error sitting alongside a transient one still means the request was
|
|
43
|
+
// wrong, and resending it cannot make it right.
|
|
44
|
+
testSync("refuses to resend a batch that mixes a real error in", () =>
|
|
45
|
+
expect(
|
|
46
|
+
Seed_Client.isTransient(
|
|
47
|
+
errors([errorOfType("InternalFailure"), errorOfType("ValidationException")]),
|
|
48
|
+
),
|
|
49
|
+
)->toBe(false)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// `Array.every` is vacuously true on an empty array, which would turn "the
|
|
53
|
+
// endpoint said errors but named none" into an endless resend.
|
|
54
|
+
testSync("treats an empty or untyped error list as final", () => {
|
|
55
|
+
expect(Seed_Client.isTransient(errors([])))->toBe(false)
|
|
56
|
+
expect(
|
|
57
|
+
Seed_Client.isTransient(errors([Dict.fromArray([("message", JSON.Encode.string("boom"))])])),
|
|
58
|
+
)->toBe(false)
|
|
59
|
+
expect(Seed_Client.isTransient(JSON.Encode.null))->toBe(false)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// An account's listed groups are not what authorizes it — the token's are. Both
|
|
64
|
+
// platforms narrow a token to the single role the caller last chose, so a seed
|
|
65
|
+
// can log in as an account the user list shows as `[Admin, Shopper]` and present
|
|
66
|
+
// a token carrying `Shopper` alone. The refusal that follows names the field and
|
|
67
|
+
// not the caller, and the account list cannot explain it either. These pin the
|
|
68
|
+
// reading-back that turns that into a sentence.
|
|
69
|
+
|
|
70
|
+
let b64url = payload =>
|
|
71
|
+
payload
|
|
72
|
+
->NodeBuffer.fromStringUtf8
|
|
73
|
+
->NodeBuffer.toStringBase64Url
|
|
74
|
+
|
|
75
|
+
// A REAL JOSE header, not the literal string "header". The difference is the
|
|
76
|
+
// whole bug: `{"kid":…,"alg":"RS256"}` is base64url JSON and decodes exactly as
|
|
77
|
+
// the payload does, so `claims` took it as the claims set and found no groups
|
|
78
|
+
// and no id in any Cognito token ever issued. A fake header that happened not to
|
|
79
|
+
// decode is what let every assertion below pass against broken code.
|
|
80
|
+
let joseHeader = b64url(`{"kid":"abc123","alg":"RS256"}`)
|
|
81
|
+
|
|
82
|
+
let jwt = claims =>
|
|
83
|
+
`${joseHeader}.${b64url(JSON.stringify(JSON.Encode.object(Dict.fromArray(claims))))}.signature`
|
|
84
|
+
|
|
85
|
+
let localToken = claims =>
|
|
86
|
+
`${b64url(JSON.stringify(JSON.Encode.object(Dict.fromArray(claims))))}.signature`
|
|
87
|
+
|
|
88
|
+
let strings = values => JSON.Encode.array(values->Array.map(JSON.Encode.string))
|
|
89
|
+
|
|
90
|
+
let clientWith = token => {
|
|
91
|
+
let c = Seed_Client.make(~config={endpoint: "http://example.invalid/graphql"})
|
|
92
|
+
c->Seed_Client.useToken(token)
|
|
93
|
+
c
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe("Seed_Client.claims:", () => {
|
|
97
|
+
// The regression this file could not previously express. A JWT header is a
|
|
98
|
+
// decodable JSON object sitting in front of the payload, so "the first segment
|
|
99
|
+
// that parses" is the header on every real token — and the seed then reported
|
|
100
|
+
// no groups, printed no `Acting as:` line, and attached no identity to a
|
|
101
|
+
// refusal, which is exactly the diagnosis it exists to give.
|
|
102
|
+
testSync("skips the JOSE header and reads the payload behind it", () =>
|
|
103
|
+
expect(
|
|
104
|
+
clientWith(jwt([("sub", JSON.Encode.string("real-sub"))]))
|
|
105
|
+
->Seed_Client.claims
|
|
106
|
+
->Option.flatMap(c => c->Dict.get("sub"))
|
|
107
|
+
->Option.flatMap(JSON.Decode.string),
|
|
108
|
+
)->toEqual(Some("real-sub"))
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// The header is identified by `alg`, not by position — a two-segment local dev
|
|
112
|
+
// token has no header at all, and its payload must not be mistaken for one.
|
|
113
|
+
testSync("takes the first segment when there is no header", () =>
|
|
114
|
+
expect(
|
|
115
|
+
clientWith(localToken([("userId", JSON.Encode.string("local-shopper"))]))
|
|
116
|
+
->Seed_Client.claims
|
|
117
|
+
->Option.flatMap(c => c->Dict.get("userId"))
|
|
118
|
+
->Option.flatMap(JSON.Decode.string),
|
|
119
|
+
)->toEqual(Some("local-shopper"))
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe("Seed_Client.effectiveGroups:", () => {
|
|
124
|
+
// A Cognito id token carries its payload in the second segment, a local dev
|
|
125
|
+
// token in the first. Neither platform should have to announce which it is.
|
|
126
|
+
testSync("reads the payload wherever the provider put it", () => {
|
|
127
|
+
expect(
|
|
128
|
+
clientWith(
|
|
129
|
+
jwt([("cognito:groups", strings(["Admin", "Shopper"]))]),
|
|
130
|
+
)->Seed_Client.effectiveGroups,
|
|
131
|
+
)->toEqual(Some(["Admin", "Shopper"]))
|
|
132
|
+
expect(
|
|
133
|
+
clientWith(localToken([("groups", strings(["Merchandiser"]))]))->Seed_Client.effectiveGroups,
|
|
134
|
+
)->toEqual(Some(["Merchandiser"]))
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// An opaque bearer is not an error — it just means this can add nothing, and
|
|
138
|
+
// the failure reports what it always did.
|
|
139
|
+
testSync("says nothing about a token it cannot read", () => {
|
|
140
|
+
expect(clientWith("not-a-token")->Seed_Client.effectiveGroups)->toEqual(None)
|
|
141
|
+
expect(
|
|
142
|
+
Seed_Client.make(
|
|
143
|
+
~config={endpoint: "http://example.invalid/graphql"},
|
|
144
|
+
)->Seed_Client.effectiveGroups,
|
|
145
|
+
)->toEqual(None)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// The id an `@owner` field is stamped with. A seed that cannot read it can only
|
|
150
|
+
// key owner-scoped rows to a literal, and a literal matches one platform's
|
|
151
|
+
// accounts at most — which is how a demo shopper came to be registered on a
|
|
152
|
+
// deployment where nobody can ever be that shopper.
|
|
153
|
+
|
|
154
|
+
describe("Seed_Client.callerId:", () => {
|
|
155
|
+
// A Cognito id token says `sub`; the local dev token is a base64url
|
|
156
|
+
// `Identity.t`, whose field is `userId`. Neither platform announces which.
|
|
157
|
+
testSync("reads the id under whichever name the provider gave it", () => {
|
|
158
|
+
expect(
|
|
159
|
+
clientWith(jwt([("sub", JSON.Encode.string("4275e4a4-00c1-70e1"))]))->Seed_Client.callerId,
|
|
160
|
+
)->toEqual(Some("4275e4a4-00c1-70e1"))
|
|
161
|
+
expect(
|
|
162
|
+
clientWith(
|
|
163
|
+
localToken([("userId", JSON.Encode.string("local-shopper"))]),
|
|
164
|
+
)->Seed_Client.callerId,
|
|
165
|
+
)->toEqual(Some("local-shopper"))
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// `sub` first, so a token carrying both is read the way the pool that minted
|
|
169
|
+
// it means it.
|
|
170
|
+
testSync("prefers sub when a token carries both names", () =>
|
|
171
|
+
expect(
|
|
172
|
+
clientWith(
|
|
173
|
+
jwt([
|
|
174
|
+
("sub", JSON.Encode.string("f215e4b4-30f1-700c")),
|
|
175
|
+
("userId", JSON.Encode.string("local-shopper")),
|
|
176
|
+
]),
|
|
177
|
+
)->Seed_Client.callerId,
|
|
178
|
+
)->toEqual(Some("f215e4b4-30f1-700c"))
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// `None` is not an error — it means this cannot add anything, and the caller
|
|
182
|
+
// falls back to whatever the accounts file declared.
|
|
183
|
+
testSync("says nothing about a token it cannot read, or one naming no id", () => {
|
|
184
|
+
expect(clientWith("not-a-token")->Seed_Client.callerId)->toEqual(None)
|
|
185
|
+
expect(
|
|
186
|
+
clientWith(jwt([("cognito:groups", strings(["Admin"]))]))->Seed_Client.callerId,
|
|
187
|
+
)->toEqual(None)
|
|
188
|
+
expect(
|
|
189
|
+
Seed_Client.make(~config={endpoint: "http://example.invalid/graphql"})->Seed_Client.callerId,
|
|
190
|
+
)->toEqual(None)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// A claim of the wrong shape is not an id. Taking the array's first element
|
|
194
|
+
// would be a guess, and a guessed owner id seeds rows nobody can read.
|
|
195
|
+
testSync("refuses a claim that is not a single string", () =>
|
|
196
|
+
expect(clientWith(jwt([("sub", strings(["a", "b"]))]))->Seed_Client.callerId)->toEqual(None)
|
|
197
|
+
)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe("Seed_Client.identitySummary:", () => {
|
|
201
|
+
// The case that cost the debugging: eligible in the user list, refused by the
|
|
202
|
+
// token. Naming what it was narrowed FROM is what points at the role switch.
|
|
203
|
+
testSync("names the membership a narrowed token gave up", () =>
|
|
204
|
+
expect(
|
|
205
|
+
clientWith(
|
|
206
|
+
jwt([
|
|
207
|
+
("cognito:groups", strings(["Shopper"])),
|
|
208
|
+
("availableRoles", JSON.Encode.string("Admin,Shopper")),
|
|
209
|
+
]),
|
|
210
|
+
)->Seed_Client.identitySummary,
|
|
211
|
+
)->toEqual(Some("Shopper — narrowed to this role from Admin, Shopper"))
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
// An ordinary login is not narrowed and has nothing to explain.
|
|
215
|
+
testSync("stays quiet about an unnarrowed token", () =>
|
|
216
|
+
expect(
|
|
217
|
+
clientWith(
|
|
218
|
+
jwt([("cognito:groups", strings(["Admin", "Shopper"]))]),
|
|
219
|
+
)->Seed_Client.identitySummary,
|
|
220
|
+
)->toEqual(Some("Admin, Shopper"))
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
// A token granting nothing reads as a sentence rather than an empty string.
|
|
224
|
+
testSync("says so when the token carries no groups at all", () =>
|
|
225
|
+
expect(
|
|
226
|
+
clientWith(jwt([("cognito:groups", strings([]))]))->Seed_Client.identitySummary,
|
|
227
|
+
)->toEqual(Some("no groups"))
|
|
228
|
+
)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// Whether a token is narrowed cannot be read off the group claim: a bearer
|
|
232
|
+
// carrying one group looks identical whether the account holds one role or five.
|
|
233
|
+
// `availableRoles` is written only when a role was chosen, so its presence IS
|
|
234
|
+
// the narrowing — and that is what decides whether the seed has anything to ask.
|
|
235
|
+
|
|
236
|
+
describe("Seed_Client.narrowedFrom:", () => {
|
|
237
|
+
testSync("names the membership a narrowed token was reduced from", () =>
|
|
238
|
+
expect(
|
|
239
|
+
clientWith(
|
|
240
|
+
jwt([
|
|
241
|
+
("cognito:groups", strings(["Shopper"])),
|
|
242
|
+
("availableRoles", JSON.Encode.string("Admin,Shopper")),
|
|
243
|
+
]),
|
|
244
|
+
)->Seed_Client.narrowedFrom,
|
|
245
|
+
)->toEqual(Some(["Admin", "Shopper"]))
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
// The case that must NOT prompt: a single-role account is not narrowed, it is
|
|
249
|
+
// simply small. Asking it to choose would put a question in every run.
|
|
250
|
+
testSync("says nothing about an unnarrowed token", () => {
|
|
251
|
+
expect(
|
|
252
|
+
clientWith(jwt([("cognito:groups", strings(["Shopper"]))]))->Seed_Client.narrowedFrom,
|
|
253
|
+
)->toEqual(None)
|
|
254
|
+
expect(
|
|
255
|
+
clientWith(
|
|
256
|
+
jwt([("cognito:groups", strings(["Admin", "Shopper"]))]),
|
|
257
|
+
)->Seed_Client.narrowedFrom,
|
|
258
|
+
)->toEqual(None)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// An empty claim is not a narrowing to nothing — it is a claim carrying no
|
|
262
|
+
// information, and offering a menu of no roles would be a dead end.
|
|
263
|
+
testSync("treats an empty availableRoles as not narrowed", () =>
|
|
264
|
+
expect(
|
|
265
|
+
clientWith(jwt([("availableRoles", JSON.Encode.string(""))]))->Seed_Client.narrowedFrom,
|
|
266
|
+
)->toEqual(None)
|
|
267
|
+
)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
describe("Seed_Client.isDenied:", () => {
|
|
271
|
+
// AppSync types it; the local server carries it in the message.
|
|
272
|
+
testSync("recognises a refusal in either platform's vocabulary", () => {
|
|
273
|
+
expect(Seed_Client.isDenied(errors([errorOfType("Unauthorized")])))->toBe(true)
|
|
274
|
+
expect(
|
|
275
|
+
Seed_Client.isDenied(
|
|
276
|
+
errors([Dict.fromArray([("message", JSON.Encode.string("Not Authorized to access X"))])]),
|
|
277
|
+
),
|
|
278
|
+
)->toBe(true)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// Attaching an identity to an unrelated failure would point the reader at the
|
|
282
|
+
// wrong thing.
|
|
283
|
+
testSync("leaves every other failure alone", () => {
|
|
284
|
+
expect(Seed_Client.isDenied(errors([errorOfType("ValidationException")])))->toBe(false)
|
|
285
|
+
expect(Seed_Client.isDenied(errors([])))->toBe(false)
|
|
286
|
+
})
|
|
287
|
+
})
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Seed_Client$ReventlessSeed from "../src/Seed_Client.res.mjs";
|
|
6
|
+
|
|
7
|
+
function errors(entries) {
|
|
8
|
+
return entries.map(prim => prim);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function errorOfType(errorType) {
|
|
12
|
+
return Object.fromEntries([[
|
|
13
|
+
"errorType",
|
|
14
|
+
errorType
|
|
15
|
+
]]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
globalThis.describe("Seed_Client.isTransient:", () => {
|
|
19
|
+
globalThis.test("resends a fault on the endpoint's own side", () => {
|
|
20
|
+
[
|
|
21
|
+
"InternalFailure",
|
|
22
|
+
"ServiceUnavailable",
|
|
23
|
+
"Throttling",
|
|
24
|
+
"TooManyRequestsException"
|
|
25
|
+
].forEach(t => {
|
|
26
|
+
let entries = [errorOfType(t)];
|
|
27
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isTransient(entries.map(prim => prim))).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
globalThis.test("resends the shape AppSync actually returned", () => {
|
|
31
|
+
let payload = JSON.parse(`[{"errorType":"InternalFailure","message":"An internal failure occurred."}]`);
|
|
32
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isTransient(payload)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
globalThis.test("fails fast on an error that is an answer", () => {
|
|
35
|
+
[
|
|
36
|
+
"ValidationException",
|
|
37
|
+
"Unauthorized",
|
|
38
|
+
"FieldUndefined"
|
|
39
|
+
].forEach(t => {
|
|
40
|
+
let entries = [errorOfType(t)];
|
|
41
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isTransient(entries.map(prim => prim))).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
globalThis.test("refuses to resend a batch that mixes a real error in", () => {
|
|
45
|
+
let entries = [
|
|
46
|
+
errorOfType("InternalFailure"),
|
|
47
|
+
errorOfType("ValidationException")
|
|
48
|
+
];
|
|
49
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isTransient(entries.map(prim => prim))).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
globalThis.test("treats an empty or untyped error list as final", () => {
|
|
52
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isTransient([].map(prim => prim))).toBe(false);
|
|
53
|
+
let entries = [Object.fromEntries([[
|
|
54
|
+
"message",
|
|
55
|
+
"boom"
|
|
56
|
+
]])];
|
|
57
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isTransient(entries.map(prim => prim))).toBe(false);
|
|
58
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isTransient(null)).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function b64url(payload) {
|
|
63
|
+
return Buffer.from(payload, "utf8").toString("base64url");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let joseHeader = Buffer.from(`{"kid":"abc123","alg":"RS256"}`, "utf8").toString("base64url");
|
|
67
|
+
|
|
68
|
+
function jwt(claims) {
|
|
69
|
+
let payload = JSON.stringify(Object.fromEntries(claims));
|
|
70
|
+
return joseHeader + `.` + Buffer.from(payload, "utf8").toString("base64url") + `.signature`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function localToken(claims) {
|
|
74
|
+
let payload = JSON.stringify(Object.fromEntries(claims));
|
|
75
|
+
return Buffer.from(payload, "utf8").toString("base64url") + `.signature`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function strings(values) {
|
|
79
|
+
return values.map(prim => prim);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function clientWith(token) {
|
|
83
|
+
let c = Seed_Client$ReventlessSeed.make({
|
|
84
|
+
endpoint: "http://example.invalid/graphql"
|
|
85
|
+
});
|
|
86
|
+
Seed_Client$ReventlessSeed.useToken(c, token);
|
|
87
|
+
return c;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
globalThis.describe("Seed_Client.claims:", () => {
|
|
91
|
+
globalThis.test("skips the JOSE header and reads the payload behind it", () => {
|
|
92
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Seed_Client$ReventlessSeed.claims(clientWith(jwt([[
|
|
93
|
+
"sub",
|
|
94
|
+
"real-sub"
|
|
95
|
+
]]))), c => c["sub"]), Stdlib_JSON.Decode.string)).toEqual("real-sub");
|
|
96
|
+
});
|
|
97
|
+
globalThis.test("takes the first segment when there is no header", () => {
|
|
98
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Seed_Client$ReventlessSeed.claims(clientWith(localToken([[
|
|
99
|
+
"userId",
|
|
100
|
+
"local-shopper"
|
|
101
|
+
]]))), c => c["userId"]), Stdlib_JSON.Decode.string)).toEqual("local-shopper");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
globalThis.describe("Seed_Client.effectiveGroups:", () => {
|
|
106
|
+
globalThis.test("reads the payload wherever the provider put it", () => {
|
|
107
|
+
globalThis.expect(Seed_Client$ReventlessSeed.effectiveGroups(clientWith(jwt([[
|
|
108
|
+
"cognito:groups",
|
|
109
|
+
[
|
|
110
|
+
"Admin",
|
|
111
|
+
"Shopper"
|
|
112
|
+
].map(prim => prim)
|
|
113
|
+
]])))).toEqual([
|
|
114
|
+
"Admin",
|
|
115
|
+
"Shopper"
|
|
116
|
+
]);
|
|
117
|
+
globalThis.expect(Seed_Client$ReventlessSeed.effectiveGroups(clientWith(localToken([[
|
|
118
|
+
"groups",
|
|
119
|
+
["Merchandiser"].map(prim => prim)
|
|
120
|
+
]])))).toEqual(["Merchandiser"]);
|
|
121
|
+
});
|
|
122
|
+
globalThis.test("says nothing about a token it cannot read", () => {
|
|
123
|
+
globalThis.expect(Seed_Client$ReventlessSeed.effectiveGroups(clientWith("not-a-token"))).toEqual(undefined);
|
|
124
|
+
globalThis.expect(Seed_Client$ReventlessSeed.effectiveGroups(Seed_Client$ReventlessSeed.make({
|
|
125
|
+
endpoint: "http://example.invalid/graphql"
|
|
126
|
+
}))).toEqual(undefined);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
globalThis.describe("Seed_Client.callerId:", () => {
|
|
131
|
+
globalThis.test("reads the id under whichever name the provider gave it", () => {
|
|
132
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith(jwt([[
|
|
133
|
+
"sub",
|
|
134
|
+
"4275e4a4-00c1-70e1"
|
|
135
|
+
]])))).toEqual("4275e4a4-00c1-70e1");
|
|
136
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith(localToken([[
|
|
137
|
+
"userId",
|
|
138
|
+
"local-shopper"
|
|
139
|
+
]])))).toEqual("local-shopper");
|
|
140
|
+
});
|
|
141
|
+
globalThis.test("prefers sub when a token carries both names", () => {
|
|
142
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith(jwt([
|
|
143
|
+
[
|
|
144
|
+
"sub",
|
|
145
|
+
"f215e4b4-30f1-700c"
|
|
146
|
+
],
|
|
147
|
+
[
|
|
148
|
+
"userId",
|
|
149
|
+
"local-shopper"
|
|
150
|
+
]
|
|
151
|
+
])))).toEqual("f215e4b4-30f1-700c");
|
|
152
|
+
});
|
|
153
|
+
globalThis.test("says nothing about a token it cannot read, or one naming no id", () => {
|
|
154
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith("not-a-token"))).toEqual(undefined);
|
|
155
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith(jwt([[
|
|
156
|
+
"cognito:groups",
|
|
157
|
+
["Admin"].map(prim => prim)
|
|
158
|
+
]])))).toEqual(undefined);
|
|
159
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(Seed_Client$ReventlessSeed.make({
|
|
160
|
+
endpoint: "http://example.invalid/graphql"
|
|
161
|
+
}))).toEqual(undefined);
|
|
162
|
+
});
|
|
163
|
+
globalThis.test("refuses a claim that is not a single string", () => {
|
|
164
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith(jwt([[
|
|
165
|
+
"sub",
|
|
166
|
+
[
|
|
167
|
+
"a",
|
|
168
|
+
"b"
|
|
169
|
+
].map(prim => prim)
|
|
170
|
+
]])))).toEqual(undefined);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
globalThis.describe("Seed_Client.identitySummary:", () => {
|
|
175
|
+
globalThis.test("names the membership a narrowed token gave up", () => {
|
|
176
|
+
globalThis.expect(Seed_Client$ReventlessSeed.identitySummary(clientWith(jwt([
|
|
177
|
+
[
|
|
178
|
+
"cognito:groups",
|
|
179
|
+
["Shopper"].map(prim => prim)
|
|
180
|
+
],
|
|
181
|
+
[
|
|
182
|
+
"availableRoles",
|
|
183
|
+
"Admin,Shopper"
|
|
184
|
+
]
|
|
185
|
+
])))).toEqual("Shopper — narrowed to this role from Admin, Shopper");
|
|
186
|
+
});
|
|
187
|
+
globalThis.test("stays quiet about an unnarrowed token", () => {
|
|
188
|
+
globalThis.expect(Seed_Client$ReventlessSeed.identitySummary(clientWith(jwt([[
|
|
189
|
+
"cognito:groups",
|
|
190
|
+
[
|
|
191
|
+
"Admin",
|
|
192
|
+
"Shopper"
|
|
193
|
+
].map(prim => prim)
|
|
194
|
+
]])))).toEqual("Admin, Shopper");
|
|
195
|
+
});
|
|
196
|
+
globalThis.test("says so when the token carries no groups at all", () => {
|
|
197
|
+
globalThis.expect(Seed_Client$ReventlessSeed.identitySummary(clientWith(jwt([[
|
|
198
|
+
"cognito:groups",
|
|
199
|
+
[].map(prim => prim)
|
|
200
|
+
]])))).toEqual("no groups");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
globalThis.describe("Seed_Client.narrowedFrom:", () => {
|
|
205
|
+
globalThis.test("names the membership a narrowed token was reduced from", () => {
|
|
206
|
+
globalThis.expect(Seed_Client$ReventlessSeed.narrowedFrom(clientWith(jwt([
|
|
207
|
+
[
|
|
208
|
+
"cognito:groups",
|
|
209
|
+
["Shopper"].map(prim => prim)
|
|
210
|
+
],
|
|
211
|
+
[
|
|
212
|
+
"availableRoles",
|
|
213
|
+
"Admin,Shopper"
|
|
214
|
+
]
|
|
215
|
+
])))).toEqual([
|
|
216
|
+
"Admin",
|
|
217
|
+
"Shopper"
|
|
218
|
+
]);
|
|
219
|
+
});
|
|
220
|
+
globalThis.test("says nothing about an unnarrowed token", () => {
|
|
221
|
+
globalThis.expect(Seed_Client$ReventlessSeed.narrowedFrom(clientWith(jwt([[
|
|
222
|
+
"cognito:groups",
|
|
223
|
+
["Shopper"].map(prim => prim)
|
|
224
|
+
]])))).toEqual(undefined);
|
|
225
|
+
globalThis.expect(Seed_Client$ReventlessSeed.narrowedFrom(clientWith(jwt([[
|
|
226
|
+
"cognito:groups",
|
|
227
|
+
[
|
|
228
|
+
"Admin",
|
|
229
|
+
"Shopper"
|
|
230
|
+
].map(prim => prim)
|
|
231
|
+
]])))).toEqual(undefined);
|
|
232
|
+
});
|
|
233
|
+
globalThis.test("treats an empty availableRoles as not narrowed", () => {
|
|
234
|
+
globalThis.expect(Seed_Client$ReventlessSeed.narrowedFrom(clientWith(jwt([[
|
|
235
|
+
"availableRoles",
|
|
236
|
+
""
|
|
237
|
+
]])))).toEqual(undefined);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
globalThis.describe("Seed_Client.isDenied:", () => {
|
|
242
|
+
globalThis.test("recognises a refusal in either platform's vocabulary", () => {
|
|
243
|
+
let entries = [errorOfType("Unauthorized")];
|
|
244
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isDenied(entries.map(prim => prim))).toBe(true);
|
|
245
|
+
let entries$1 = [Object.fromEntries([[
|
|
246
|
+
"message",
|
|
247
|
+
"Not Authorized to access X"
|
|
248
|
+
]])];
|
|
249
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isDenied(entries$1.map(prim => prim))).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
globalThis.test("leaves every other failure alone", () => {
|
|
252
|
+
let entries = [errorOfType("ValidationException")];
|
|
253
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isDenied(entries.map(prim => prim))).toBe(false);
|
|
254
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isDenied([].map(prim => prim))).toBe(false);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
export {
|
|
259
|
+
errors,
|
|
260
|
+
errorOfType,
|
|
261
|
+
b64url,
|
|
262
|
+
joseHeader,
|
|
263
|
+
jwt,
|
|
264
|
+
localToken,
|
|
265
|
+
strings,
|
|
266
|
+
clientWith,
|
|
267
|
+
}
|
|
268
|
+
/* Not a pure module */
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// `SEED_ROLE` is how a run says what to act as when nobody can answer a prompt —
|
|
4
|
+
// a CI run wedged by a stored narrowing, or a deliberate restricted run.
|
|
5
|
+
//
|
|
6
|
+
// It carries two different intentions in one variable, which is why the parsing
|
|
7
|
+
// is worth pinning: naming a role narrows to it, and the "give me everything"
|
|
8
|
+
// answer has several spellings because an operator setting it by hand will write
|
|
9
|
+
// whichever one comes to mind. Getting that wrong is not a typo — `SEED_ROLE=full`
|
|
10
|
+
// read as a role named "full" would ask the platform to act as a group nobody
|
|
11
|
+
// holds, and be refused.
|
|
12
|
+
|
|
13
|
+
let withEnv = (value: option<string>, f: unit => unit) => {
|
|
14
|
+
let key = "SEED_ROLE"
|
|
15
|
+
let previous = NodeProcess.env->Dict.get(key)
|
|
16
|
+
switch value {
|
|
17
|
+
| Some(v) => NodeProcess.env->Dict.set(key, v)
|
|
18
|
+
| None => NodeProcess.env->Dict.delete(key)
|
|
19
|
+
}
|
|
20
|
+
f()
|
|
21
|
+
switch previous {
|
|
22
|
+
| Some(v) => NodeProcess.env->Dict.set(key, v)
|
|
23
|
+
| None => NodeProcess.env->Dict.delete(key)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("Seed_Connect.roleFromEnv:", () => {
|
|
28
|
+
testSync("is absent when nothing asked for a role", () =>
|
|
29
|
+
withEnv(None, () => expect(Seed_Connect.roleFromEnv())->Expect.toEqual(None))
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
testSync("reads a named role as a narrowing to it", () =>
|
|
33
|
+
withEnv(
|
|
34
|
+
Some("Shopper"),
|
|
35
|
+
() =>
|
|
36
|
+
expect(Seed_Connect.roleFromEnv())->Expect.toEqual(Some(Seed_Connect.Narrowed("Shopper"))),
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// Every spelling of "everything" an operator might reach for. They all mean
|
|
41
|
+
// clear the stored choice, which is what seeding wants.
|
|
42
|
+
testSync("takes each spelling of full membership as a clear", () =>
|
|
43
|
+
["full", "all", "none", "clear", "FULL", "All"]->Array.forEach(
|
|
44
|
+
v =>
|
|
45
|
+
withEnv(
|
|
46
|
+
Some(v),
|
|
47
|
+
() => expect(Seed_Connect.roleFromEnv())->Expect.toEqual(Some(Seed_Connect.Full)),
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// Role names are case-sensitive to the platform (`mayActAs` compares exactly),
|
|
53
|
+
// so a named role is passed through untouched — only the clear vocabulary is
|
|
54
|
+
// case-folded.
|
|
55
|
+
testSync("passes a role name through with its case intact", () =>
|
|
56
|
+
withEnv(
|
|
57
|
+
Some("Merchandiser"),
|
|
58
|
+
() =>
|
|
59
|
+
expect(Seed_Connect.roleFromEnv())->Expect.toEqual(
|
|
60
|
+
Some(Seed_Connect.Narrowed("Merchandiser")),
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
4
|
+
import * as Seed_Connect$ReventlessSeed from "../src/Seed_Connect.res.mjs";
|
|
5
|
+
|
|
6
|
+
function withEnv(value, f) {
|
|
7
|
+
let key = "SEED_ROLE";
|
|
8
|
+
let previous = process.env[key];
|
|
9
|
+
if (value !== undefined) {
|
|
10
|
+
process.env[key] = value;
|
|
11
|
+
} else {
|
|
12
|
+
Stdlib_Dict.$$delete(process.env, key);
|
|
13
|
+
}
|
|
14
|
+
f();
|
|
15
|
+
if (previous !== undefined) {
|
|
16
|
+
process.env[key] = previous;
|
|
17
|
+
return;
|
|
18
|
+
} else {
|
|
19
|
+
return Stdlib_Dict.$$delete(process.env, key);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
globalThis.describe("Seed_Connect.roleFromEnv:", () => {
|
|
24
|
+
globalThis.test("is absent when nothing asked for a role", () => withEnv(undefined, () => {
|
|
25
|
+
globalThis.expect(Seed_Connect$ReventlessSeed.roleFromEnv()).toEqual(undefined);
|
|
26
|
+
}));
|
|
27
|
+
globalThis.test("reads a named role as a narrowing to it", () => withEnv("Shopper", () => {
|
|
28
|
+
globalThis.expect(Seed_Connect$ReventlessSeed.roleFromEnv()).toEqual({
|
|
29
|
+
TAG: "Narrowed",
|
|
30
|
+
_0: "Shopper"
|
|
31
|
+
});
|
|
32
|
+
}));
|
|
33
|
+
globalThis.test("takes each spelling of full membership as a clear", () => {
|
|
34
|
+
[
|
|
35
|
+
"full",
|
|
36
|
+
"all",
|
|
37
|
+
"none",
|
|
38
|
+
"clear",
|
|
39
|
+
"FULL",
|
|
40
|
+
"All"
|
|
41
|
+
].forEach(v => withEnv(v, () => {
|
|
42
|
+
globalThis.expect(Seed_Connect$ReventlessSeed.roleFromEnv()).toEqual("Full");
|
|
43
|
+
}));
|
|
44
|
+
});
|
|
45
|
+
globalThis.test("passes a role name through with its case intact", () => withEnv("Merchandiser", () => {
|
|
46
|
+
globalThis.expect(Seed_Connect$ReventlessSeed.roleFromEnv()).toEqual({
|
|
47
|
+
TAG: "Narrowed",
|
|
48
|
+
_0: "Merchandiser"
|
|
49
|
+
});
|
|
50
|
+
}));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
withEnv,
|
|
55
|
+
}
|
|
56
|
+
/* Not a pure module */
|
|
Binary file
|
|
Binary file
|