sounding 0.0.3 → 0.1.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/README.md +336 -1
- package/bin/sounding.js +156 -0
- package/index.js +23 -0
- package/lib/create-app-manager.js +380 -26
- package/lib/create-auth-helpers.js +168 -21
- package/lib/create-browser-manager.js +578 -31
- package/lib/create-error.js +35 -0
- package/lib/create-expect.js +1070 -27
- package/lib/create-helper-runner.js +38 -2
- package/lib/create-mail-capture.js +174 -25
- package/lib/create-mailbox.js +20 -0
- package/lib/create-request-client.js +635 -57
- package/lib/create-runtime.js +222 -21
- package/lib/create-socket-manager.js +706 -0
- package/lib/create-test-api.js +491 -102
- package/lib/create-visit-client.js +40 -2
- package/lib/create-world-engine.js +106 -7
- package/lib/create-world-loader.js +150 -8
- package/lib/default-config.js +26 -0
- package/lib/define-world.js +27 -2
- package/lib/init-project.js +403 -0
- package/lib/merge-config.js +11 -0
- package/lib/normalize-config.js +16 -19
- package/lib/resolve-auth-config.js +36 -0
- package/lib/resolve-datastore.js +50 -7
- package/lib/resolve-dependency.js +145 -0
- package/lib/test-runner.js +427 -0
- package/lib/trial-context.js +29 -0
- package/lib/types.js +675 -0
- package/lib/validate-config.js +633 -0
- package/lib/validate-test-args.js +480 -0
- package/package.json +16 -2
|
@@ -1,15 +1,54 @@
|
|
|
1
1
|
const { resolveAuthConfig } = require('./resolve-auth-config')
|
|
2
|
-
|
|
2
|
+
const { createSoundingError } = require('./create-error')
|
|
3
|
+
|
|
4
|
+
/** @typedef {import('./types').SoundingActor} SoundingActor */
|
|
5
|
+
/** @typedef {import('./types').SoundingAuthHelpers} SoundingAuthHelpers */
|
|
6
|
+
/** @typedef {import('./types').SoundingBrowserLoginResult} SoundingBrowserLoginResult */
|
|
7
|
+
/** @typedef {import('./types').SoundingMagicLink} SoundingMagicLink */
|
|
8
|
+
/** @typedef {import('./types').SoundingMailbox} SoundingMailbox */
|
|
9
|
+
/** @typedef {import('./types').SoundingPage} SoundingPage */
|
|
10
|
+
/** @typedef {import('./types').SoundingPasswordRequestResult} SoundingPasswordRequestResult */
|
|
11
|
+
/** @typedef {import('./types').SoundingRequestMagicLinkResult} SoundingRequestMagicLinkResult */
|
|
12
|
+
/** @typedef {import('./types').SoundingRequestOptions} SoundingRequestOptions */
|
|
13
|
+
/** @typedef {import('./types').SoundingRequestClient} SoundingRequestClient */
|
|
14
|
+
/** @typedef {import('./types').SoundingSailsApp} SoundingSailsApp */
|
|
15
|
+
/** @typedef {import('./types').SoundingWorldEngine} SoundingWorldEngine */
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {any} value
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
3
21
|
function normalizeEmail(value) {
|
|
4
22
|
return String(value || '')
|
|
5
23
|
.trim()
|
|
6
24
|
.toLowerCase()
|
|
7
25
|
}
|
|
8
26
|
|
|
27
|
+
/**
|
|
28
|
+
* @param {any} value
|
|
29
|
+
* @returns {value is string}
|
|
30
|
+
*/
|
|
9
31
|
function looksLikeEmail(value) {
|
|
10
32
|
return typeof value === 'string' && value.includes('@')
|
|
11
33
|
}
|
|
12
34
|
|
|
35
|
+
/**
|
|
36
|
+
* @param {string[]} values
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function formatAvailable(values) {
|
|
40
|
+
return values.length ? values.join(', ') : 'none'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {{
|
|
45
|
+
* sails?: SoundingSailsApp,
|
|
46
|
+
* world: SoundingWorldEngine,
|
|
47
|
+
* mailbox: SoundingMailbox,
|
|
48
|
+
* request: SoundingRequestClient,
|
|
49
|
+
* }} input
|
|
50
|
+
* @returns {SoundingAuthHelpers}
|
|
51
|
+
*/
|
|
13
52
|
function createAuthHelpers({ sails, world, mailbox, request }) {
|
|
14
53
|
function getAuthConfig() {
|
|
15
54
|
return resolveAuthConfig({ sails })
|
|
@@ -19,6 +58,10 @@ function createAuthHelpers({ sails, world, mailbox, request }) {
|
|
|
19
58
|
return getAuthConfig().model
|
|
20
59
|
}
|
|
21
60
|
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} alias
|
|
63
|
+
* @returns {SoundingActor | null}
|
|
64
|
+
*/
|
|
22
65
|
function resolveWorldActor(alias) {
|
|
23
66
|
if (!alias || typeof alias !== 'string') {
|
|
24
67
|
return null
|
|
@@ -34,20 +77,50 @@ function createAuthHelpers({ sails, world, mailbox, request }) {
|
|
|
34
77
|
)
|
|
35
78
|
}
|
|
36
79
|
|
|
80
|
+
/**
|
|
81
|
+
* @returns {string[]}
|
|
82
|
+
*/
|
|
83
|
+
function availableWorldActors() {
|
|
84
|
+
const auth = getAuthConfig()
|
|
85
|
+
const aliases = new Set()
|
|
86
|
+
|
|
87
|
+
for (const collection of [auth.worldCollection, 'users', 'creators']) {
|
|
88
|
+
const entries = world.current?.[collection]
|
|
89
|
+
|
|
90
|
+
if (entries && typeof entries === 'object' && !Array.isArray(entries)) {
|
|
91
|
+
for (const alias of Object.keys(entries)) {
|
|
92
|
+
aliases.add(alias)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return Array.from(aliases).sort()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {string} email
|
|
102
|
+
* @param {string} [fullName]
|
|
103
|
+
* @returns {Promise<Record<string, any>>}
|
|
104
|
+
*/
|
|
37
105
|
async function createUserFromEmail(email, fullName) {
|
|
38
106
|
const auth = getAuthConfig()
|
|
39
107
|
const User = getAuthModel()
|
|
40
108
|
|
|
41
109
|
if (auth.modelIdentity !== 'user') {
|
|
42
|
-
throw
|
|
43
|
-
|
|
44
|
-
|
|
110
|
+
throw createSoundingError({
|
|
111
|
+
code: 'E_SOUNDING_AUTH_CREATE_UNSUPPORTED',
|
|
112
|
+
message: `Sounding auth helpers could not auto-create a missing ${auth.modelIdentity} record.`,
|
|
113
|
+
details: {
|
|
114
|
+
modelIdentity: auth.modelIdentity,
|
|
115
|
+
},
|
|
116
|
+
})
|
|
45
117
|
}
|
|
46
118
|
|
|
47
119
|
if (!sails?.helpers?.user?.signupWithTeam) {
|
|
48
|
-
throw
|
|
49
|
-
|
|
50
|
-
|
|
120
|
+
throw createSoundingError({
|
|
121
|
+
code: 'E_SOUNDING_AUTH_SIGNUP_HELPER_MISSING',
|
|
122
|
+
message: 'Sounding auth helpers could not find `sails.helpers.user.signupWithTeam`.',
|
|
123
|
+
})
|
|
51
124
|
}
|
|
52
125
|
|
|
53
126
|
const signupResult = await sails.helpers.user.signupWithTeam.with({
|
|
@@ -70,53 +143,101 @@ function createAuthHelpers({ sails, world, mailbox, request }) {
|
|
|
70
143
|
return signupResult.user
|
|
71
144
|
}
|
|
72
145
|
|
|
146
|
+
/**
|
|
147
|
+
* @param {SoundingActor | string} actorOrEmail
|
|
148
|
+
* @param {{ createIfMissing?: boolean, fullName?: string, [key: string]: any }} [options]
|
|
149
|
+
* @returns {Promise<Record<string, any>>}
|
|
150
|
+
*/
|
|
73
151
|
async function resolveActor(actorOrEmail, options = {}) {
|
|
74
152
|
const auth = getAuthConfig()
|
|
75
153
|
const User = getAuthModel()
|
|
76
154
|
|
|
77
155
|
if (!actorOrEmail) {
|
|
78
|
-
throw
|
|
156
|
+
throw createSoundingError({
|
|
157
|
+
code: 'E_SOUNDING_AUTH_ACTOR_REQUIRED',
|
|
158
|
+
message: 'Sounding auth helpers require an actor or email address.',
|
|
159
|
+
})
|
|
79
160
|
}
|
|
80
161
|
|
|
81
162
|
let candidate = actorOrEmail
|
|
163
|
+
let unresolvedAlias = null
|
|
82
164
|
|
|
83
165
|
if (typeof candidate === 'string' && !looksLikeEmail(candidate)) {
|
|
166
|
+
unresolvedAlias = candidate
|
|
84
167
|
candidate = resolveWorldActor(candidate) || candidate
|
|
85
168
|
}
|
|
86
169
|
|
|
87
|
-
if (candidate?.id && User?.findOne) {
|
|
170
|
+
if (typeof candidate !== 'string' && candidate?.id && User?.findOne) {
|
|
88
171
|
return User.findOne({ id: candidate.id }) || candidate
|
|
89
172
|
}
|
|
90
173
|
|
|
91
|
-
const email =
|
|
174
|
+
const email =
|
|
175
|
+
typeof candidate === 'string'
|
|
176
|
+
? looksLikeEmail(candidate)
|
|
177
|
+
? candidate
|
|
178
|
+
: null
|
|
179
|
+
: candidate?.email || null
|
|
92
180
|
|
|
93
181
|
if (!email) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
182
|
+
const details = {
|
|
183
|
+
actor: candidate,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (unresolvedAlias && candidate === unresolvedAlias) {
|
|
187
|
+
const availableActors = availableWorldActors()
|
|
188
|
+
details.availableActors = availableActors
|
|
189
|
+
|
|
190
|
+
throw createSoundingError({
|
|
191
|
+
code: 'E_SOUNDING_AUTH_EMAIL_UNRESOLVED',
|
|
192
|
+
message: `Sounding auth helpers could not resolve an email address for actor \`${candidate}\`. Available actors: ${formatAvailable(availableActors)}.`,
|
|
193
|
+
details,
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
throw createSoundingError({
|
|
198
|
+
code: 'E_SOUNDING_AUTH_EMAIL_UNRESOLVED',
|
|
199
|
+
message: `Sounding auth helpers could not resolve an email address for actor \`${candidate}\`.`,
|
|
200
|
+
details,
|
|
201
|
+
})
|
|
97
202
|
}
|
|
98
203
|
|
|
99
204
|
const normalizedEmail = normalizeEmail(email)
|
|
100
205
|
let user = User?.findOne ? await User.findOne({ email: normalizedEmail }) : null
|
|
101
206
|
|
|
102
207
|
if (!user && options.createIfMissing !== false) {
|
|
103
|
-
user = await createUserFromEmail(
|
|
208
|
+
user = await createUserFromEmail(
|
|
209
|
+
normalizedEmail,
|
|
210
|
+
(typeof candidate !== 'string' && candidate?.fullName) || options.fullName
|
|
211
|
+
)
|
|
104
212
|
}
|
|
105
213
|
|
|
106
214
|
if (!user) {
|
|
107
|
-
throw
|
|
108
|
-
|
|
109
|
-
|
|
215
|
+
throw createSoundingError({
|
|
216
|
+
code: 'E_SOUNDING_AUTH_MODEL_UNAVAILABLE',
|
|
217
|
+
message: `Sounding auth helpers could not find a ${auth.modelIdentity} for ${normalizedEmail}.`,
|
|
218
|
+
details: {
|
|
219
|
+
modelIdentity: auth.modelIdentity,
|
|
220
|
+
email: normalizedEmail,
|
|
221
|
+
},
|
|
222
|
+
})
|
|
110
223
|
}
|
|
111
224
|
|
|
112
225
|
return user
|
|
113
226
|
}
|
|
114
227
|
|
|
228
|
+
/**
|
|
229
|
+
* @param {SoundingActor | string} actorOrEmail
|
|
230
|
+
* @param {{ createIfMissing?: boolean, fullName?: string, [key: string]: any }} [options]
|
|
231
|
+
* @returns {Promise<SoundingMagicLink>}
|
|
232
|
+
*/
|
|
115
233
|
async function issueMagicLink(actorOrEmail, options = {}) {
|
|
116
234
|
const User = getAuthModel()
|
|
117
235
|
|
|
118
236
|
if (!User?.updateOne) {
|
|
119
|
-
throw
|
|
237
|
+
throw createSoundingError({
|
|
238
|
+
code: 'E_SOUNDING_AUTH_MODEL_UNAVAILABLE',
|
|
239
|
+
message: 'Sounding auth helpers require an auth model with updateOne().',
|
|
240
|
+
})
|
|
120
241
|
}
|
|
121
242
|
|
|
122
243
|
const user = await resolveActor(actorOrEmail, {
|
|
@@ -143,6 +264,11 @@ function createAuthHelpers({ sails, world, mailbox, request }) {
|
|
|
143
264
|
}
|
|
144
265
|
}
|
|
145
266
|
|
|
267
|
+
/**
|
|
268
|
+
* @param {SoundingActor | string} actorOrEmail
|
|
269
|
+
* @param {{ fullName?: string, redirectUrl?: string, requestOptions?: SoundingRequestOptions, [key: string]: any }} [options]
|
|
270
|
+
* @returns {Promise<SoundingRequestMagicLinkResult>}
|
|
271
|
+
*/
|
|
146
272
|
async function requestMagicLink(actorOrEmail, options = {}) {
|
|
147
273
|
const user = await resolveActor(actorOrEmail, {
|
|
148
274
|
createIfMissing: true,
|
|
@@ -167,9 +293,18 @@ function createAuthHelpers({ sails, world, mailbox, request }) {
|
|
|
167
293
|
}
|
|
168
294
|
}
|
|
169
295
|
|
|
296
|
+
/**
|
|
297
|
+
* @param {SoundingActor | string} actorOrEmail
|
|
298
|
+
* @param {SoundingPage} page
|
|
299
|
+
* @param {{ password?: string, rememberMe?: boolean, returnUrl?: string, [key: string]: any }} [options]
|
|
300
|
+
* @returns {Promise<SoundingBrowserLoginResult>}
|
|
301
|
+
*/
|
|
170
302
|
async function loginWithPassword(actorOrEmail, page, options = {}) {
|
|
171
303
|
if (!page || typeof page.goto !== 'function') {
|
|
172
|
-
throw
|
|
304
|
+
throw createSoundingError({
|
|
305
|
+
code: 'E_SOUNDING_AUTH_BROWSER_PAGE_REQUIRED',
|
|
306
|
+
message: 'Sounding password browser login requires a Playwright page.',
|
|
307
|
+
})
|
|
173
308
|
}
|
|
174
309
|
|
|
175
310
|
const auth = getAuthConfig()
|
|
@@ -180,7 +315,10 @@ function createAuthHelpers({ sails, world, mailbox, request }) {
|
|
|
180
315
|
const email = normalizeEmail(actor?.email || actorOrEmail)
|
|
181
316
|
|
|
182
317
|
if (!options.password) {
|
|
183
|
-
throw
|
|
318
|
+
throw createSoundingError({
|
|
319
|
+
code: 'E_SOUNDING_AUTH_PASSWORD_REQUIRED',
|
|
320
|
+
message: 'Sounding password login requires a `password` option.',
|
|
321
|
+
})
|
|
184
322
|
}
|
|
185
323
|
|
|
186
324
|
const loginUrl = new URL(auth.password.pagePath, 'http://sounding.local')
|
|
@@ -212,6 +350,11 @@ function createAuthHelpers({ sails, world, mailbox, request }) {
|
|
|
212
350
|
}
|
|
213
351
|
}
|
|
214
352
|
|
|
353
|
+
/**
|
|
354
|
+
* @param {SoundingActor | string} actorOrEmail
|
|
355
|
+
* @param {{ password?: string, rememberMe?: boolean, returnUrl?: string, request?: SoundingRequestClient, requestOptions?: SoundingRequestOptions, [key: string]: any }} [options]
|
|
356
|
+
* @returns {Promise<SoundingPasswordRequestResult>}
|
|
357
|
+
*/
|
|
215
358
|
async function requestWithPassword(actorOrEmail, options = {}) {
|
|
216
359
|
const auth = getAuthConfig()
|
|
217
360
|
const actor = await resolveActor(actorOrEmail, {
|
|
@@ -221,9 +364,13 @@ function createAuthHelpers({ sails, world, mailbox, request }) {
|
|
|
221
364
|
const email = normalizeEmail(actor?.email || actorOrEmail)
|
|
222
365
|
|
|
223
366
|
if (!options.password) {
|
|
224
|
-
throw
|
|
367
|
+
throw createSoundingError({
|
|
368
|
+
code: 'E_SOUNDING_AUTH_PASSWORD_REQUIRED',
|
|
369
|
+
message: 'Sounding password request auth requires a `password` option.',
|
|
370
|
+
})
|
|
225
371
|
}
|
|
226
372
|
|
|
373
|
+
/** @type {Record<string, any>} */
|
|
227
374
|
const payload = {
|
|
228
375
|
[auth.password.form.email]: email,
|
|
229
376
|
[auth.password.form.password]: options.password,
|