@pikku/inspector 0.12.49 → 0.12.52
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 +501 -0
- package/OPTIMIZATION-PLAN.md +9 -3
- package/dist/add/add-auth.d.ts +1 -1
- package/dist/add/add-auth.js +1 -1
- package/dist/add/add-cli.js +7 -0
- package/dist/add/add-credential.js +1 -1
- package/dist/add/add-functions.js +156 -7
- package/dist/add/add-http-route.js +16 -6
- package/dist/add/add-middleware.js +46 -3
- package/dist/add/add-personas.d.ts +13 -0
- package/dist/add/add-personas.js +210 -0
- package/dist/add/add-scope.js +1 -1
- package/dist/add/add-secret.js +1 -1
- package/dist/add/add-system-role.d.ts +2 -0
- package/dist/add/add-system-role.js +102 -0
- package/dist/add/add-variable.js +1 -1
- package/dist/add/add-wire-addon.js +31 -0
- package/dist/error-codes.d.ts +10 -1
- package/dist/error-codes.js +11 -0
- package/dist/index.d.ts +1 -1
- package/dist/inspector.js +33 -1
- package/dist/types.d.ts +46 -2
- package/dist/utils/annotate-http-route-auth.d.ts +22 -0
- package/dist/utils/annotate-http-route-auth.js +48 -0
- package/dist/utils/extract-function-name.js +13 -4
- package/dist/utils/extract-function-node.js +18 -1
- package/dist/utils/extract-secret-usage.d.ts +9 -0
- package/dist/utils/extract-secret-usage.js +61 -0
- package/dist/utils/literal-properties.d.ts +22 -0
- package/dist/utils/literal-properties.js +69 -0
- package/dist/utils/load-addon-functions-meta.js +26 -1
- package/dist/utils/permissions.js +8 -0
- package/dist/utils/post-process.d.ts +68 -3
- package/dist/utils/post-process.js +243 -13
- package/dist/utils/schema-generator.d.ts +24 -0
- package/dist/utils/schema-generator.js +118 -19
- package/dist/utils/secret-alias-services.d.ts +8 -0
- package/dist/utils/secret-alias-services.js +45 -0
- package/dist/utils/secret-brokers.d.ts +2 -0
- package/dist/utils/secret-brokers.js +9 -0
- package/dist/utils/serialize-inspector-state.d.ts +10 -0
- package/dist/utils/serialize-inspector-state.js +18 -0
- package/dist/utils/serialize-middleware-groups-meta.js +3 -0
- package/dist/utils/validate-exposed-functions-gated.d.ts +14 -0
- package/dist/utils/validate-exposed-functions-gated.js +85 -0
- package/dist/utils/validate-tags-resolve-to-middleware.d.ts +20 -0
- package/dist/utils/validate-tags-resolve-to-middleware.js +41 -0
- package/dist/visit.js +4 -0
- package/package.json +3 -3
- package/src/add/add-auth.test.ts +5 -3
- package/src/add/add-auth.ts +1 -1
- package/src/add/add-cli.ts +8 -0
- package/src/add/add-credential.ts +1 -1
- package/src/add/add-functions.test.ts +263 -1
- package/src/add/add-functions.ts +219 -9
- package/src/add/add-http-route.ts +20 -7
- package/src/add/add-middleware.test.ts +196 -0
- package/src/add/add-middleware.ts +64 -4
- package/src/add/add-personas.test.ts +268 -0
- package/src/add/add-personas.ts +276 -0
- package/src/add/add-scenario-step.test.ts +49 -3
- package/src/add/add-scope.test.ts +30 -30
- package/src/add/add-scope.ts +1 -1
- package/src/add/add-secret.ts +1 -1
- package/src/add/add-subject-scenario-steps.test.ts +161 -0
- package/src/add/add-system-role.test.ts +214 -0
- package/src/add/add-system-role.ts +164 -0
- package/src/add/add-variable.ts +1 -1
- package/src/add/add-wire-addon.test.ts +111 -0
- package/src/add/add-wire-addon.ts +29 -0
- package/src/add/addon-bans.test.ts +4 -4
- package/src/add/config-value-metadata.test.ts +11 -11
- package/src/add/wire-name-literal.test.ts +2 -2
- package/src/error-codes.ts +13 -0
- package/src/index.ts +4 -1
- package/src/inspector.ts +39 -0
- package/src/types.ts +47 -2
- package/src/utils/annotate-http-route-auth.test.ts +163 -0
- package/src/utils/annotate-http-route-auth.ts +52 -0
- package/src/utils/extract-function-name.ts +14 -4
- package/src/utils/extract-function-node.ts +18 -6
- package/src/utils/extract-secret-usage.test.ts +81 -0
- package/src/utils/extract-secret-usage.ts +73 -0
- package/src/utils/literal-properties.ts +97 -0
- package/src/utils/load-addon-functions-meta.ts +32 -2
- package/src/utils/permissions-shorthand.test.ts +79 -0
- package/src/utils/permissions.ts +7 -0
- package/src/utils/post-process.test.ts +133 -0
- package/src/utils/post-process.ts +310 -12
- package/src/utils/schema-cache.test.ts +124 -0
- package/src/utils/schema-generator.test.ts +62 -1
- package/src/utils/schema-generator.ts +138 -21
- package/src/utils/secret-alias-services.test.ts +146 -0
- package/src/utils/secret-alias-services.ts +58 -0
- package/src/utils/secret-brokers.test.ts +53 -0
- package/src/utils/secret-brokers.ts +11 -0
- package/src/utils/serialize-inspector-state.ts +28 -0
- package/src/utils/serialize-middleware-groups-meta.ts +3 -0
- package/src/utils/validate-exposed-functions-gated.test.ts +230 -0
- package/src/utils/validate-exposed-functions-gated.ts +94 -0
- package/src/utils/validate-tags-resolve-to-middleware.test.ts +105 -0
- package/src/utils/validate-tags-resolve-to-middleware.ts +48 -0
- package/src/visit.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import { DISPOSITIONS } from '@pikku/core/virtual-user'
|
|
3
|
+
import { isRunnablePersona } from '@pikku/core/persona'
|
|
4
|
+
import type {
|
|
5
|
+
PersonaAccountMeta,
|
|
6
|
+
PersonaMeta,
|
|
7
|
+
} from '@pikku/core/persona'
|
|
8
|
+
import { ErrorCode } from '../error-codes.js'
|
|
9
|
+
import type { AddWiring, InspectorLogger } from '../types.js'
|
|
10
|
+
import {
|
|
11
|
+
booleanProperty,
|
|
12
|
+
getProperty,
|
|
13
|
+
numberProperty,
|
|
14
|
+
stringArrayProperty,
|
|
15
|
+
stringProperty,
|
|
16
|
+
unwrapAs,
|
|
17
|
+
} from '../utils/literal-properties.js'
|
|
18
|
+
|
|
19
|
+
type Tuning = NonNullable<PersonaMeta['tuning']>
|
|
20
|
+
|
|
21
|
+
/** A dial and the range outside which it is a mistake rather than a choice. */
|
|
22
|
+
const RATES: [keyof Tuning, number, number][] = [
|
|
23
|
+
['temperature', 0, 2],
|
|
24
|
+
['repeatRate', 0, 1],
|
|
25
|
+
['reReadRate', 0, 1],
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `{ moves, temperature, repeatRate, … }` — a disposition's dials, overridden.
|
|
30
|
+
*
|
|
31
|
+
* The ranges are checked here rather than at run time because these are the
|
|
32
|
+
* kind of typo that does not throw: `repeatRate: 18` meaning 18% would silently
|
|
33
|
+
* double every call the persona makes, and you would read the run as a product
|
|
34
|
+
* bug.
|
|
35
|
+
*/
|
|
36
|
+
const tuningProperty = (
|
|
37
|
+
config: ts.ObjectLiteralExpression,
|
|
38
|
+
onError: (message: string) => void
|
|
39
|
+
): Tuning | undefined => {
|
|
40
|
+
const value = getProperty(config, 'tuning')
|
|
41
|
+
if (!value || !ts.isObjectLiteralExpression(value)) {
|
|
42
|
+
return undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tuning: Tuning = {}
|
|
46
|
+
|
|
47
|
+
const movesValue = getProperty(value, 'moves')
|
|
48
|
+
if (movesValue && ts.isObjectLiteralExpression(movesValue)) {
|
|
49
|
+
const moves: NonNullable<Tuning['moves']> = {}
|
|
50
|
+
for (const move of ['continue', 'suspend', 'resume', 'abandon'] as const) {
|
|
51
|
+
const weight = numberProperty(movesValue, move)
|
|
52
|
+
if (weight === undefined) {
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
if (weight < 0) {
|
|
56
|
+
onError(`tuning.moves.${move} must not be negative (got ${weight}).`)
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
moves[move] = weight
|
|
60
|
+
}
|
|
61
|
+
if (Object.keys(moves).length) {
|
|
62
|
+
tuning.moves = moves
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const [name, min, max] of RATES) {
|
|
67
|
+
const rate = numberProperty(value, name)
|
|
68
|
+
if (rate === undefined) {
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
if (rate < min || rate > max) {
|
|
72
|
+
onError(`tuning.${name} must be between ${min} and ${max} (got ${rate}).`)
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
Object.assign(tuning, { [name]: rate })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const flag of ['emptyMemory', 'readOnly', 'invertedOracle'] as const) {
|
|
79
|
+
const declared = booleanProperty(value, flag)
|
|
80
|
+
if (declared !== undefined) {
|
|
81
|
+
tuning[flag] = declared
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const instructions = stringProperty(value, 'instructions')
|
|
86
|
+
if (instructions !== undefined) {
|
|
87
|
+
tuning.instructions = instructions
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Object.keys(tuning).length ? tuning : undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const accountProperty = (
|
|
94
|
+
value: ts.Expression | undefined
|
|
95
|
+
): PersonaAccountMeta | undefined => {
|
|
96
|
+
if (!value || !ts.isObjectLiteralExpression(value)) {
|
|
97
|
+
return undefined
|
|
98
|
+
}
|
|
99
|
+
const provider = stringProperty(value, 'provider')
|
|
100
|
+
return provider === undefined ? {} : { provider }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const linkedAccountsProperty = (
|
|
104
|
+
config: ts.ObjectLiteralExpression,
|
|
105
|
+
personaId: string,
|
|
106
|
+
logger: InspectorLogger
|
|
107
|
+
): Record<string, PersonaAccountMeta> | undefined => {
|
|
108
|
+
const value = getProperty(config, 'linkedAccounts')
|
|
109
|
+
if (!value || !ts.isObjectLiteralExpression(value)) {
|
|
110
|
+
return undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const accounts: Record<string, PersonaAccountMeta> = {}
|
|
114
|
+
for (const prop of value.properties) {
|
|
115
|
+
if (!ts.isPropertyAssignment(prop)) {
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
|
|
119
|
+
logger.critical(
|
|
120
|
+
ErrorCode.NON_LITERAL_WIRE_NAME,
|
|
121
|
+
`Persona '${personaId}' has a linked account whose key is not a literal.`
|
|
122
|
+
)
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
const account = accountProperty(prop.initializer)
|
|
126
|
+
if (!account) {
|
|
127
|
+
logger.critical(
|
|
128
|
+
ErrorCode.INVALID_VALUE,
|
|
129
|
+
`Persona '${personaId}' linked account '${prop.name.text}' must be an object literal.`
|
|
130
|
+
)
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
accounts[prop.name.text] = account
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return Object.keys(accounts).length ? accounts : undefined
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Inspector for `definePersonas()` calls.
|
|
141
|
+
*
|
|
142
|
+
* A persona is entirely declaration — a person, their roles, what they want —
|
|
143
|
+
* so there is no code body and nothing to resolve at runtime. What is read here
|
|
144
|
+
* is the whole thing, and it is generated straight to `personas.gen.json` for
|
|
145
|
+
* the CLI and the console to read without loading the app.
|
|
146
|
+
*
|
|
147
|
+
* `name` is the one field that cannot be defaulted. A persona without one is
|
|
148
|
+
* not a person, and the console has nothing to show.
|
|
149
|
+
*/
|
|
150
|
+
export const addPersonas: AddWiring = (logger, node, _checker, state) => {
|
|
151
|
+
if (!ts.isCallExpression(node)) {
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const expression = node.expression
|
|
156
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'definePersonas') {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const firstArg = node.arguments[0]
|
|
161
|
+
if (!firstArg) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const unwrapped = unwrapAs(firstArg)
|
|
166
|
+
if (!ts.isObjectLiteralExpression(unwrapped)) {
|
|
167
|
+
logger.critical(
|
|
168
|
+
ErrorCode.INVALID_VALUE,
|
|
169
|
+
'definePersonas() needs an inline object — personas are read statically, never evaluated.'
|
|
170
|
+
)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const sourceFile = node.getSourceFile().fileName
|
|
175
|
+
const known = Object.keys(DISPOSITIONS)
|
|
176
|
+
|
|
177
|
+
for (const prop of unwrapped.properties) {
|
|
178
|
+
if (!ts.isPropertyAssignment(prop)) {
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
|
|
183
|
+
logger.critical(
|
|
184
|
+
ErrorCode.NON_LITERAL_WIRE_NAME,
|
|
185
|
+
'A persona is declared with a key that is not a literal.'
|
|
186
|
+
)
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
const id = prop.name.text
|
|
190
|
+
|
|
191
|
+
if (!ts.isObjectLiteralExpression(prop.initializer)) {
|
|
192
|
+
logger.critical(
|
|
193
|
+
ErrorCode.INVALID_VALUE,
|
|
194
|
+
`Persona '${id}' must be an object literal.`
|
|
195
|
+
)
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
const config = prop.initializer
|
|
199
|
+
|
|
200
|
+
const name = stringProperty(config, 'name')
|
|
201
|
+
if (!name) {
|
|
202
|
+
logger.critical(
|
|
203
|
+
ErrorCode.INVALID_VALUE,
|
|
204
|
+
`Persona '${id}' needs a literal 'name' — a persona is a person, and the console has nothing to show without one.`
|
|
205
|
+
)
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// The generated `definePersonas` types this as a union, so a bad value only
|
|
210
|
+
// reaches here in source that does not compile. Caught anyway: a
|
|
211
|
+
// disposition is the entire behaviour, and defaulting a typo to `realistic`
|
|
212
|
+
// would run a different person than the one that was written.
|
|
213
|
+
const disposition = stringProperty(config, 'disposition')
|
|
214
|
+
if (disposition !== undefined && !known.includes(disposition)) {
|
|
215
|
+
logger.critical(
|
|
216
|
+
ErrorCode.INVALID_VALUE,
|
|
217
|
+
`Persona '${id}' has an unknown disposition '${disposition}'. One of: ${known.join(', ')}`
|
|
218
|
+
)
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let tuningFailed = false
|
|
223
|
+
const tuning = tuningProperty(config, (message) => {
|
|
224
|
+
tuningFailed = true
|
|
225
|
+
logger.critical(
|
|
226
|
+
ErrorCode.INVALID_VALUE,
|
|
227
|
+
`Persona '${id}': ${message}`
|
|
228
|
+
)
|
|
229
|
+
})
|
|
230
|
+
if (tuningFailed) {
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const account = accountProperty(getProperty(config, 'account'))
|
|
235
|
+
const declaredRunnable = booleanProperty(config, 'runnable')
|
|
236
|
+
|
|
237
|
+
const persona: PersonaMeta = {
|
|
238
|
+
id,
|
|
239
|
+
name,
|
|
240
|
+
roles: stringArrayProperty(config, 'roles') ?? [],
|
|
241
|
+
goals: stringArrayProperty(config, 'goals') ?? [],
|
|
242
|
+
tags: stringArrayProperty(config, 'tags') ?? [],
|
|
243
|
+
runnable: isRunnablePersona({
|
|
244
|
+
...(declaredRunnable !== undefined
|
|
245
|
+
? { runnable: declaredRunnable }
|
|
246
|
+
: {}),
|
|
247
|
+
...(account ? { account } : {}),
|
|
248
|
+
}),
|
|
249
|
+
sourceFile,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const jobTitle = stringProperty(config, 'jobTitle')
|
|
253
|
+
if (jobTitle !== undefined) persona.jobTitle = jobTitle
|
|
254
|
+
const description = stringProperty(config, 'description')
|
|
255
|
+
if (description !== undefined) persona.description = description
|
|
256
|
+
const personality = stringProperty(config, 'personality')
|
|
257
|
+
if (personality !== undefined) persona.personality = personality
|
|
258
|
+
if (disposition !== undefined) {
|
|
259
|
+
persona.disposition = disposition as PersonaMeta['disposition']
|
|
260
|
+
}
|
|
261
|
+
if (tuning !== undefined) persona.tuning = tuning
|
|
262
|
+
const fixtures = stringArrayProperty(config, 'fixtures')
|
|
263
|
+
if (fixtures !== undefined) persona.fixtures = fixtures
|
|
264
|
+
// Left absent rather than resolved here: "everywhere but production" is a
|
|
265
|
+
// fact about the config, and the inspector has no business baking today's
|
|
266
|
+
// environment list into a meta file that outlives it.
|
|
267
|
+
const environments = stringArrayProperty(config, 'environments')
|
|
268
|
+
if (environments !== undefined) persona.environments = environments
|
|
269
|
+
if (account !== undefined) persona.account = account
|
|
270
|
+
const linkedAccounts = linkedAccountsProperty(config, id, logger)
|
|
271
|
+
if (linkedAccounts !== undefined) persona.linkedAccounts = linkedAccounts
|
|
272
|
+
|
|
273
|
+
state.personas.files.add(sourceFile)
|
|
274
|
+
state.personas.definitions.push(persona)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -30,13 +30,13 @@ const STEPS = [
|
|
|
30
30
|
'export const buysAnApple = pikkuScenarioStep({',
|
|
31
31
|
" name: 'buysAnApple',",
|
|
32
32
|
" description: 'buys an apple',",
|
|
33
|
-
' browser:
|
|
34
|
-
'
|
|
33
|
+
' browser: async ({ logger }, data: { qty: number }) => ({ ok: data.qty > 0 }),',
|
|
34
|
+
' default: async ({ logger }, data: { qty: number }) => ({ ok: data.qty > 0 }),',
|
|
35
35
|
'})',
|
|
36
36
|
'export const seesAReceipt = pikkuScenarioStep({',
|
|
37
37
|
" name: 'seesAReceipt',",
|
|
38
38
|
" description: 'sees a receipt',",
|
|
39
|
-
'
|
|
39
|
+
' default: async ({ logger }) => ({ ok: true }),',
|
|
40
40
|
'})',
|
|
41
41
|
].join('\n')
|
|
42
42
|
|
|
@@ -209,6 +209,52 @@ describe('pikkuScenarioStep', () => {
|
|
|
209
209
|
}
|
|
210
210
|
})
|
|
211
211
|
|
|
212
|
+
test('a scenario that never asserts is a PKU680 critical', async () => {
|
|
213
|
+
const { criticals, cleanup } = await run([
|
|
214
|
+
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
215
|
+
"await scenario.when('buys an apple', 'buysAnApple', { qty: 2 }, { actor: actors.shopper })",
|
|
216
|
+
])
|
|
217
|
+
try {
|
|
218
|
+
assert.ok(
|
|
219
|
+
criticals.find((c) => c.code === 'PKU680'),
|
|
220
|
+
`a flow with no 'then' proves only that nothing threw, and contributes 0/0 to witness coverage, got: ${JSON.stringify(criticals)}`
|
|
221
|
+
)
|
|
222
|
+
} finally {
|
|
223
|
+
await cleanup()
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('a scenario with an assertion is not flagged', async () => {
|
|
228
|
+
const { criticals, cleanup } = await run([
|
|
229
|
+
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
230
|
+
"await scenario.then('sees a receipt', 'seesAReceipt', {}, { actor: actors.shopper })",
|
|
231
|
+
])
|
|
232
|
+
try {
|
|
233
|
+
assert.equal(
|
|
234
|
+
criticals.filter((c) => c.code === 'PKU680').length,
|
|
235
|
+
0,
|
|
236
|
+
`got: ${JSON.stringify(criticals)}`
|
|
237
|
+
)
|
|
238
|
+
} finally {
|
|
239
|
+
await cleanup()
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('a scenario driving only rpc steps is not flagged — it has no step ladder to assert on', async () => {
|
|
244
|
+
const { criticals, cleanup } = await run([
|
|
245
|
+
"await scenario.do('reads the order', 'orderGet', { orderId: 'o1' })",
|
|
246
|
+
])
|
|
247
|
+
try {
|
|
248
|
+
assert.equal(
|
|
249
|
+
criticals.filter((c) => c.code === 'PKU680').length,
|
|
250
|
+
0,
|
|
251
|
+
`the rule is about scenarios written as given/when/then, got: ${JSON.stringify(criticals)}`
|
|
252
|
+
)
|
|
253
|
+
} finally {
|
|
254
|
+
await cleanup()
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
|
|
212
258
|
test('a non-browser step without an actor is allowed', async () => {
|
|
213
259
|
const { criticals, cleanup } = await run([
|
|
214
260
|
"await scenario.given('sees a receipt', 'seesAReceipt', {})",
|
|
@@ -39,8 +39,8 @@ describe('addScope inspector', () => {
|
|
|
39
39
|
test('extracts a flat scope', async () => {
|
|
40
40
|
const { state, criticals } = await inspectSource(
|
|
41
41
|
[
|
|
42
|
-
"import {
|
|
43
|
-
'
|
|
42
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
43
|
+
'defineScope({',
|
|
44
44
|
' admin: {',
|
|
45
45
|
" displayName: 'Administration',",
|
|
46
46
|
" description: 'Administrative access',",
|
|
@@ -62,8 +62,8 @@ describe('addScope inspector', () => {
|
|
|
62
62
|
test('extracts a displayName at every depth', async () => {
|
|
63
63
|
const { state, criticals } = await inspectSource(
|
|
64
64
|
[
|
|
65
|
-
"import {
|
|
66
|
-
'
|
|
65
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
66
|
+
'defineScope({',
|
|
67
67
|
' admin: {',
|
|
68
68
|
" displayName: 'Administration',",
|
|
69
69
|
' scopes: {',
|
|
@@ -90,8 +90,8 @@ describe('addScope inspector', () => {
|
|
|
90
90
|
test('extracts a nested scope tree', async () => {
|
|
91
91
|
const { state, criticals } = await inspectSource(
|
|
92
92
|
[
|
|
93
|
-
"import {
|
|
94
|
-
'
|
|
93
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
94
|
+
'defineScope({',
|
|
95
95
|
' admin: {',
|
|
96
96
|
' scopes: {',
|
|
97
97
|
' invoices: {',
|
|
@@ -120,8 +120,8 @@ describe('addScope inspector', () => {
|
|
|
120
120
|
test('records the source file', async () => {
|
|
121
121
|
const { state, file } = await inspectSource(
|
|
122
122
|
[
|
|
123
|
-
"import {
|
|
124
|
-
'
|
|
123
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
124
|
+
'defineScope({ admin: {} })',
|
|
125
125
|
].join('\n')
|
|
126
126
|
)
|
|
127
127
|
|
|
@@ -134,8 +134,8 @@ describe('addScope inspector', () => {
|
|
|
134
134
|
test('extracts several roots from one declaration', async () => {
|
|
135
135
|
const { state, criticals } = await inspectSource(
|
|
136
136
|
[
|
|
137
|
-
"import {
|
|
138
|
-
'
|
|
137
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
138
|
+
'defineScope({ admin: {}, billing: { scopes: { read: {} } } })',
|
|
139
139
|
].join('\n')
|
|
140
140
|
)
|
|
141
141
|
|
|
@@ -150,9 +150,9 @@ describe('addScope inspector', () => {
|
|
|
150
150
|
test('extracts several declarations', async () => {
|
|
151
151
|
const { state } = await inspectSource(
|
|
152
152
|
[
|
|
153
|
-
"import {
|
|
154
|
-
'
|
|
155
|
-
'
|
|
153
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
154
|
+
'defineScope({ admin: {} })',
|
|
155
|
+
'defineScope({ billing: {} })',
|
|
156
156
|
].join('\n')
|
|
157
157
|
)
|
|
158
158
|
|
|
@@ -165,8 +165,8 @@ describe('addScope inspector', () => {
|
|
|
165
165
|
test('is critical when a root embeds the separator', async () => {
|
|
166
166
|
const { criticals } = await inspectSource(
|
|
167
167
|
[
|
|
168
|
-
"import {
|
|
169
|
-
"
|
|
168
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
169
|
+
"defineScope({ 'admin:users': {} })",
|
|
170
170
|
].join('\n')
|
|
171
171
|
)
|
|
172
172
|
|
|
@@ -179,8 +179,8 @@ describe('addScope inspector', () => {
|
|
|
179
179
|
test('is critical when a root is the wildcard', async () => {
|
|
180
180
|
const { criticals } = await inspectSource(
|
|
181
181
|
[
|
|
182
|
-
"import {
|
|
183
|
-
"
|
|
182
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
183
|
+
"defineScope({ '*': {} })",
|
|
184
184
|
].join('\n')
|
|
185
185
|
)
|
|
186
186
|
|
|
@@ -193,9 +193,9 @@ describe('addScope inspector', () => {
|
|
|
193
193
|
test('is critical when a root key is not a literal', async () => {
|
|
194
194
|
const { criticals } = await inspectSource(
|
|
195
195
|
[
|
|
196
|
-
"import {
|
|
196
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
197
197
|
'const k = String(1)',
|
|
198
|
-
'
|
|
198
|
+
'defineScope({ [k]: {} } as any)',
|
|
199
199
|
].join('\n')
|
|
200
200
|
)
|
|
201
201
|
|
|
@@ -208,8 +208,8 @@ describe('addScope inspector', () => {
|
|
|
208
208
|
test('is critical when a root is not an object literal', async () => {
|
|
209
209
|
const { criticals } = await inspectSource(
|
|
210
210
|
[
|
|
211
|
-
"import {
|
|
212
|
-
"
|
|
211
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
212
|
+
"defineScope({ admin: 'nope' } as any)",
|
|
213
213
|
].join('\n')
|
|
214
214
|
)
|
|
215
215
|
|
|
@@ -223,7 +223,7 @@ describe('addScope inspector', () => {
|
|
|
223
223
|
describe('validateScopeReferences', () => {
|
|
224
224
|
const funcSource = (scopes: string, decls: string[] = []) =>
|
|
225
225
|
[
|
|
226
|
-
"import {
|
|
226
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
227
227
|
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
228
228
|
...decls,
|
|
229
229
|
'export const f = pikkuSessionlessFunc({',
|
|
@@ -235,7 +235,7 @@ describe('validateScopeReferences', () => {
|
|
|
235
235
|
test('accepts a declared scope', async () => {
|
|
236
236
|
const { criticals } = await inspectSource(
|
|
237
237
|
funcSource("['admin:invoices:create']", [
|
|
238
|
-
'
|
|
238
|
+
'defineScope({',
|
|
239
239
|
' admin: {',
|
|
240
240
|
' scopes: { invoices: { scopes: { create: {} } } },',
|
|
241
241
|
' },',
|
|
@@ -249,7 +249,7 @@ describe('validateScopeReferences', () => {
|
|
|
249
249
|
test('accepts an intermediate node of a declared tree', async () => {
|
|
250
250
|
const { criticals } = await inspectSource(
|
|
251
251
|
funcSource("['admin:invoices']", [
|
|
252
|
-
'
|
|
252
|
+
'defineScope({',
|
|
253
253
|
' admin: {',
|
|
254
254
|
' scopes: { invoices: { scopes: { create: {} } } },',
|
|
255
255
|
' },',
|
|
@@ -262,7 +262,7 @@ describe('validateScopeReferences', () => {
|
|
|
262
262
|
|
|
263
263
|
test('rejects an undeclared scope', async () => {
|
|
264
264
|
const { criticals } = await inspectSource(
|
|
265
|
-
funcSource("['billing:read']", ['
|
|
265
|
+
funcSource("['billing:read']", ['defineScope({ admin: {} })'])
|
|
266
266
|
)
|
|
267
267
|
|
|
268
268
|
assert.ok(
|
|
@@ -277,7 +277,7 @@ describe('validateScopeReferences', () => {
|
|
|
277
277
|
|
|
278
278
|
test('lists the available scopes when one is undeclared', async () => {
|
|
279
279
|
const { criticals } = await inspectSource(
|
|
280
|
-
funcSource("['nope']", ['
|
|
280
|
+
funcSource("['nope']", ['defineScope({ admin: {} })'])
|
|
281
281
|
)
|
|
282
282
|
|
|
283
283
|
assert.ok(
|
|
@@ -289,7 +289,7 @@ describe('validateScopeReferences', () => {
|
|
|
289
289
|
test('rejects a typo in a nested scope', async () => {
|
|
290
290
|
const { criticals } = await inspectSource(
|
|
291
291
|
funcSource("['admin:invoice:create']", [
|
|
292
|
-
'
|
|
292
|
+
'defineScope({',
|
|
293
293
|
' admin: {',
|
|
294
294
|
' scopes: { invoices: { scopes: { create: {} } } },',
|
|
295
295
|
' },',
|
|
@@ -305,7 +305,7 @@ describe('validateScopeReferences', () => {
|
|
|
305
305
|
|
|
306
306
|
test('accepts a wildcard requirement whose node is declared', async () => {
|
|
307
307
|
const { criticals } = await inspectSource(
|
|
308
|
-
funcSource("['admin:*']", ['
|
|
308
|
+
funcSource("['admin:*']", ['defineScope({ admin: {} })'])
|
|
309
309
|
)
|
|
310
310
|
|
|
311
311
|
assert.deepEqual(criticals, [])
|
|
@@ -313,7 +313,7 @@ describe('validateScopeReferences', () => {
|
|
|
313
313
|
|
|
314
314
|
test('rejects a wildcard requirement whose node is undeclared', async () => {
|
|
315
315
|
const { criticals } = await inspectSource(
|
|
316
|
-
funcSource("['billing:*']", ['
|
|
316
|
+
funcSource("['billing:*']", ['defineScope({ admin: {} })'])
|
|
317
317
|
)
|
|
318
318
|
|
|
319
319
|
assert.ok(
|
|
@@ -324,7 +324,7 @@ describe('validateScopeReferences', () => {
|
|
|
324
324
|
|
|
325
325
|
test('rejects a bare wildcard requirement', async () => {
|
|
326
326
|
const { criticals } = await inspectSource(
|
|
327
|
-
funcSource("['*']", ['
|
|
327
|
+
funcSource("['*']", ['defineScope({ admin: {} })'])
|
|
328
328
|
)
|
|
329
329
|
|
|
330
330
|
assert.ok(
|
package/src/add/add-scope.ts
CHANGED
|
@@ -142,7 +142,7 @@ export const addScope: AddWiring = (logger, node, checker, state, _options) => {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
const expression = node.expression
|
|
145
|
-
if (!ts.isIdentifier(expression) || expression.text !== '
|
|
145
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'defineScope') {
|
|
146
146
|
return
|
|
147
147
|
}
|
|
148
148
|
|
package/src/add/add-secret.ts
CHANGED