@live-change/user-frontend 0.9.196 → 0.9.197
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/e2e/connectEmailCode.test.ts +65 -0
- package/e2e/connectEmailLink.test.ts +62 -0
- package/e2e/delete.test.ts +52 -0
- package/e2e/disconnectEmail.test.ts +43 -0
- package/e2e/env.ts +130 -0
- package/e2e/resetPasswordWithEmailCode.test.ts +60 -0
- package/e2e/resetPasswordWithEmailLink.test.ts +60 -0
- package/e2e/setPassword.test.ts +70 -0
- package/e2e/signInEmailCode.test.ts +58 -0
- package/e2e/signInEmailLink.test.ts +58 -0
- package/e2e/signInEmailPassword.test.ts +51 -0
- package/e2e/signOut.test.ts +45 -0
- package/e2e/signUpEmailCode.test.ts +46 -0
- package/e2e/signUpEmailLink.test.ts +46 -0
- package/e2e/steps.ts +179 -0
- package/e2e/withBrowser.ts +19 -0
- package/package.json +27 -27
- package/server/app.config.js +33 -2
- package/e2e/codecept.conf.js +0 -60
- package/e2e/connectEmailCode.test.js +0 -61
- package/e2e/connectEmailLink.test.js +0 -60
- package/e2e/delete.test.js +0 -44
- package/e2e/disconnectEmail.test.js +0 -42
- package/e2e/resetPasswordWithEmailCode.test.js +0 -62
- package/e2e/resetPasswordWithEmailLink.test.js +0 -62
- package/e2e/setPassword.test.js +0 -70
- package/e2e/signInEmailCode.test.js +0 -52
- package/e2e/signInEmailLink.test.js +0 -52
- package/e2e/signInEmailPassword.test.js +0 -47
- package/e2e/signOut.test.js +0 -41
- package/e2e/signUpEmailCode.test.js +0 -41
- package/e2e/signUpEmailLink.test.js +0 -42
- package/e2e/steps.d.ts +0 -12
- package/e2e/steps_file.js +0 -156
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import App from '@live-change/framework'
|
|
4
|
+
import randomProfile from 'random-profile-generator'
|
|
5
|
+
import { withBrowser } from './withBrowser.js'
|
|
6
|
+
import { useSecretCode, sleep } from './steps.js'
|
|
7
|
+
|
|
8
|
+
const app = App.app()
|
|
9
|
+
const email = randomProfile.profile().firstName.toLowerCase() + '@test.com'
|
|
10
|
+
const email2 = randomProfile.profile().firstName.toLowerCase() + '2@test.com'
|
|
11
|
+
const happyPath = false
|
|
12
|
+
|
|
13
|
+
test('connect email with code', async () => {
|
|
14
|
+
await withBrowser(async (page, env) => {
|
|
15
|
+
const user = app.generateUid()
|
|
16
|
+
|
|
17
|
+
const User = env.haveModel('user', 'User')
|
|
18
|
+
const Email = env.haveModel('email', 'Email')
|
|
19
|
+
const AuthenticatedUser = env.haveModel('user', 'AuthenticatedUser')
|
|
20
|
+
|
|
21
|
+
await User.create({ id: user, roles: [] })
|
|
22
|
+
await Email.create({ id: email, email, user })
|
|
23
|
+
await page.goto(env.url + '/', { waitUntil: 'networkidle' })
|
|
24
|
+
const session = await page.evaluate(
|
|
25
|
+
() => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
|
|
26
|
+
)
|
|
27
|
+
await AuthenticatedUser.create({ id: session, user, session })
|
|
28
|
+
|
|
29
|
+
// Reload so client gets user from server; otherwise router redirects to sign-in when opening /user/settings/connected
|
|
30
|
+
await page.reload({ waitUntil: 'networkidle' })
|
|
31
|
+
await page.goto(env.url + '/user/settings/connected', { waitUntil: 'networkidle' })
|
|
32
|
+
await page.getByText(email).waitFor({ state: 'visible', timeout: 15000 })
|
|
33
|
+
assert.strictEqual(await page.getByText(email2).isVisible(), false, 'email2 should not be visible')
|
|
34
|
+
|
|
35
|
+
await page.click('button#connect')
|
|
36
|
+
assert.ok(page.url().includes('/connect'))
|
|
37
|
+
|
|
38
|
+
if (!happyPath) {
|
|
39
|
+
await page.fill('input#email', email)
|
|
40
|
+
await page.click('button[type=submit]')
|
|
41
|
+
assert.ok(page.url().includes('/connect'))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await page.fill('input#email', email2)
|
|
45
|
+
await page.click('button[type=submit]')
|
|
46
|
+
await page.waitForURL('**/sent/*', { timeout: 10000 })
|
|
47
|
+
assert.ok(page.url().includes('/sent/'))
|
|
48
|
+
|
|
49
|
+
const url = page.url()
|
|
50
|
+
const authentication = url.split('/').pop()!
|
|
51
|
+
|
|
52
|
+
const authenticationData = await env.grabObject('messageAuthentication', 'Authentication', authentication)
|
|
53
|
+
assert.ok(authenticationData, 'authentication created')
|
|
54
|
+
assert.strictEqual((authenticationData as { messageData?: { user: string } })?.messageData?.user, user, 'authentication contains user')
|
|
55
|
+
|
|
56
|
+
await useSecretCode(page, env, authentication, happyPath)
|
|
57
|
+
await page.waitForURL('**/connect-finished', { timeout: 10000 })
|
|
58
|
+
assert.ok(page.url().includes('/connect-finished'))
|
|
59
|
+
|
|
60
|
+
if (!happyPath) {
|
|
61
|
+
await page.goto(url, { waitUntil: 'networkidle' })
|
|
62
|
+
assert.ok(page.url().includes('/connect-finished'))
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import App from '@live-change/framework'
|
|
4
|
+
import randomProfile from 'random-profile-generator'
|
|
5
|
+
import { withBrowser } from './withBrowser.js'
|
|
6
|
+
import { useSecretLink } from './steps.js'
|
|
7
|
+
|
|
8
|
+
const app = App.app()
|
|
9
|
+
const email = randomProfile.profile().firstName.toLowerCase() + '@test.com'
|
|
10
|
+
const email2 = randomProfile.profile().firstName.toLowerCase() + '2@test.com'
|
|
11
|
+
const happyPath = false
|
|
12
|
+
|
|
13
|
+
test('connect email with link', async () => {
|
|
14
|
+
await withBrowser(async (page, env) => {
|
|
15
|
+
const user = app.generateUid()
|
|
16
|
+
|
|
17
|
+
const User = env.haveModel('user', 'User')
|
|
18
|
+
const Email = env.haveModel('email', 'Email')
|
|
19
|
+
const AuthenticatedUser = env.haveModel('user', 'AuthenticatedUser')
|
|
20
|
+
|
|
21
|
+
await User.create({ id: user, roles: [] })
|
|
22
|
+
await Email.create({ id: email, email, user })
|
|
23
|
+
await page.goto(env.url + '/', { waitUntil: 'networkidle' })
|
|
24
|
+
const session = await page.evaluate(
|
|
25
|
+
() => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
|
|
26
|
+
)
|
|
27
|
+
await AuthenticatedUser.create({ id: session, user, session })
|
|
28
|
+
|
|
29
|
+
await page.reload({ waitUntil: 'networkidle' })
|
|
30
|
+
await page.goto(env.url + '/user/settings/connected', { waitUntil: 'networkidle' })
|
|
31
|
+
await page.getByText(email).waitFor({ state: 'visible', timeout: 15000 })
|
|
32
|
+
assert.strictEqual(await page.getByText(email2).isVisible(), false, 'email2 should not be visible')
|
|
33
|
+
|
|
34
|
+
await page.click('button#connect')
|
|
35
|
+
assert.ok(page.url().includes('/connect'))
|
|
36
|
+
|
|
37
|
+
if (!happyPath) {
|
|
38
|
+
await page.fill('input#email', email)
|
|
39
|
+
await page.click('button[type=submit]')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await page.fill('input#email', email2)
|
|
43
|
+
await page.click('button[type=submit]')
|
|
44
|
+
assert.ok(page.url().includes('/sent/'))
|
|
45
|
+
|
|
46
|
+
const url = page.url()
|
|
47
|
+
const authentication = url.split('/').pop()!
|
|
48
|
+
|
|
49
|
+
const authenticationData = await env.grabObject('messageAuthentication', 'Authentication', authentication)
|
|
50
|
+
assert.ok(authenticationData, 'authentication created')
|
|
51
|
+
assert.strictEqual((authenticationData as { messageData?: { user: string } })?.messageData?.user, user, 'authentication contains user')
|
|
52
|
+
|
|
53
|
+
const linkData = await useSecretLink(page, env, authentication, happyPath)
|
|
54
|
+
await page.waitForURL('**/connect-finished', { timeout: 10000 })
|
|
55
|
+
assert.ok(page.url().includes('/connect-finished'))
|
|
56
|
+
|
|
57
|
+
if (!happyPath) {
|
|
58
|
+
await page.goto(env.url + '/user/link/' + linkData.secretCode, { waitUntil: 'networkidle' })
|
|
59
|
+
await page.getByText('Link used').waitFor({ state: 'visible' })
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import App from '@live-change/framework'
|
|
4
|
+
import randomProfile from 'random-profile-generator'
|
|
5
|
+
import { withBrowser } from './withBrowser.js'
|
|
6
|
+
|
|
7
|
+
const app = App.app()
|
|
8
|
+
const name = randomProfile.profile().firstName.toLowerCase()
|
|
9
|
+
const email = name + '@test.com'
|
|
10
|
+
|
|
11
|
+
test('delete account', async () => {
|
|
12
|
+
await withBrowser(async (page, env) => {
|
|
13
|
+
const user = app.generateUid()
|
|
14
|
+
|
|
15
|
+
const User = env.haveModel('user', 'User')
|
|
16
|
+
const Email = env.haveModel('email', 'Email')
|
|
17
|
+
const AuthenticatedUser = env.haveModel('user', 'AuthenticatedUser')
|
|
18
|
+
|
|
19
|
+
await User.create({ id: user, roles: [] })
|
|
20
|
+
await Email.create({ id: email, email, user })
|
|
21
|
+
await page.goto(env.url + '/', { waitUntil: 'networkidle' })
|
|
22
|
+
const session = await page.evaluate(
|
|
23
|
+
() => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
|
|
24
|
+
)
|
|
25
|
+
await AuthenticatedUser.create({ id: session, user, session })
|
|
26
|
+
|
|
27
|
+
await page.reload({ waitUntil: 'networkidle' })
|
|
28
|
+
const clientUser = await page.evaluate(
|
|
29
|
+
() => (window as unknown as { api: { client: { value: { user: string } } } }).api.client.value.user
|
|
30
|
+
)
|
|
31
|
+
assert.strictEqual(user, clientUser, 'client logged in')
|
|
32
|
+
|
|
33
|
+
await page.goto(env.url + '/user/settings/delete', { waitUntil: 'networkidle' })
|
|
34
|
+
await page.click('.p-checkbox-box')
|
|
35
|
+
await page.click('button#delete')
|
|
36
|
+
await page.waitForURL('**/delete-finished', { timeout: 10000 })
|
|
37
|
+
|
|
38
|
+
assert.ok(page.url().includes('/delete-finished'))
|
|
39
|
+
|
|
40
|
+
await new Promise((r) => setTimeout(r, 300))
|
|
41
|
+
const clientUserAfterDelete = await page.evaluate(
|
|
42
|
+
() => (window as unknown as { api: { client: { value: { user: unknown } } } }).api.client.value.user
|
|
43
|
+
)
|
|
44
|
+
assert.strictEqual(!!clientUserAfterDelete, false, 'user logged out')
|
|
45
|
+
|
|
46
|
+
const deletedUser = await User.get(user)
|
|
47
|
+
assert.strictEqual(!!deletedUser, false, 'user deleted')
|
|
48
|
+
|
|
49
|
+
const deletedEmail = await Email.get(email)
|
|
50
|
+
assert.strictEqual(!!deletedEmail, false, 'email deleted')
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import App from '@live-change/framework'
|
|
4
|
+
import randomProfile from 'random-profile-generator'
|
|
5
|
+
import { withBrowser } from './withBrowser.js'
|
|
6
|
+
|
|
7
|
+
const app = App.app()
|
|
8
|
+
const name = randomProfile.profile().firstName.toLowerCase()
|
|
9
|
+
const email = name + '@test.com'
|
|
10
|
+
const email2 = name + '2@test.com'
|
|
11
|
+
const happyPath = false
|
|
12
|
+
|
|
13
|
+
test('disconnect email', async () => {
|
|
14
|
+
await withBrowser(async (page, env) => {
|
|
15
|
+
const user = app.generateUid()
|
|
16
|
+
|
|
17
|
+
const User = env.haveModel('user', 'User')
|
|
18
|
+
const Email = env.haveModel('email', 'Email')
|
|
19
|
+
const AuthenticatedUser = env.haveModel('user', 'AuthenticatedUser')
|
|
20
|
+
|
|
21
|
+
await User.create({ id: user, roles: [] })
|
|
22
|
+
await Email.create({ id: email, email, user })
|
|
23
|
+
await Email.create({ id: email2, email: email2, user })
|
|
24
|
+
await page.goto(env.url + '/', { waitUntil: 'networkidle' })
|
|
25
|
+
const session = await page.evaluate(
|
|
26
|
+
() => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
|
|
27
|
+
)
|
|
28
|
+
await AuthenticatedUser.create({ id: session, user, session })
|
|
29
|
+
|
|
30
|
+
await page.reload({ waitUntil: 'networkidle' })
|
|
31
|
+
await page.goto(env.url + '/user/settings/connected', { waitUntil: 'networkidle' })
|
|
32
|
+
await page.getByText(email).waitFor({ state: 'visible' })
|
|
33
|
+
await page.getByText(email2).waitFor({ state: 'visible' })
|
|
34
|
+
|
|
35
|
+
await page.click('span.pi-times')
|
|
36
|
+
await page.click('text=Yes')
|
|
37
|
+
assert.strictEqual(await page.getByText(email2).isVisible(), false, 'email2 should not be visible')
|
|
38
|
+
|
|
39
|
+
if (!happyPath) {
|
|
40
|
+
await page.locator('span.pi-times').waitFor({ state: 'hidden' })
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
})
|
package/e2e/env.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { fileURLToPath } from 'url'
|
|
3
|
+
import { TestServer } from '@live-change/server'
|
|
4
|
+
import appConfig from '../server/app.config.js'
|
|
5
|
+
import * as services from '../server/services.list.js'
|
|
6
|
+
|
|
7
|
+
const READY_TIMEOUT_MS = 60000
|
|
8
|
+
const READY_POLL_MS = 2000
|
|
9
|
+
|
|
10
|
+
async function waitForServerReady(url: string): Promise<void> {
|
|
11
|
+
const deadline = Date.now() + READY_TIMEOUT_MS
|
|
12
|
+
while (Date.now() < deadline) {
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(url)
|
|
15
|
+
if (res.ok) return
|
|
16
|
+
} catch {
|
|
17
|
+
// not ready yet
|
|
18
|
+
}
|
|
19
|
+
await new Promise((r) => setTimeout(r, READY_POLL_MS))
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Server at ${url} did not become ready within ${READY_TIMEOUT_MS}ms`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
25
|
+
const serverDir = path.join(__dirname, '..', 'server')
|
|
26
|
+
const frontDir = path.join(__dirname, '..', 'front')
|
|
27
|
+
|
|
28
|
+
for (const serviceConfig of appConfig.services) {
|
|
29
|
+
const name = (serviceConfig as { name: string }).name
|
|
30
|
+
;(serviceConfig as { module?: unknown }).module = (services as Record<string, unknown>)[name]
|
|
31
|
+
}
|
|
32
|
+
;(appConfig as { init?: (s: unknown) => Promise<void> }).init = (services as { init: (s: unknown) => Promise<void> }).init
|
|
33
|
+
|
|
34
|
+
export type TestEnv = {
|
|
35
|
+
server: InstanceType<typeof TestServer>
|
|
36
|
+
url: string
|
|
37
|
+
haveService: (name: string) => { name: string; models: Record<string, { get: (id: string) => Promise<unknown> }>; views: Record<string, unknown>; actions: Record<string, unknown>; triggers: Record<string, unknown> }
|
|
38
|
+
haveModel: (serviceName: string, modelName: string) => { get: (id: string) => Promise<unknown>; create: (data: unknown) => Promise<unknown>; update: (id: string, data: unknown) => Promise<unknown>; delete: (id: string) => Promise<unknown>; indexObjectGet: (index: string, key: unknown, opts?: unknown) => Promise<unknown>; indexRangeGet: (index: string, key: unknown) => Promise<unknown[]>; definition: { properties: Record<string, { preFilter: (v: unknown) => unknown }> } }
|
|
39
|
+
haveView: (serviceName: string, viewName: string) => unknown
|
|
40
|
+
haveAction: (serviceName: string, actionName: string) => unknown
|
|
41
|
+
haveTrigger: (serviceName: string, triggerName: string) => unknown
|
|
42
|
+
grabObject: (serviceName: string, modelName: string, id: string) => Promise<unknown>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let envPromise: Promise<TestEnv> | null = null
|
|
46
|
+
|
|
47
|
+
function haveService(server: InstanceType<typeof TestServer>, name: string) {
|
|
48
|
+
const service = server.apiServer.services.services.find((s: { name: string }) => s.name === name)
|
|
49
|
+
if (!service) throw new Error('service ' + name + ' not found')
|
|
50
|
+
return service
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function haveModel(server: InstanceType<typeof TestServer>, serviceName: string, modelName: string) {
|
|
54
|
+
const service = haveService(server, serviceName)
|
|
55
|
+
const model = service.models[modelName]
|
|
56
|
+
if (!model) throw new Error('model ' + modelName + ' not found')
|
|
57
|
+
return model
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function getTestEnv(): Promise<TestEnv> {
|
|
61
|
+
if (envPromise) return envPromise
|
|
62
|
+
envPromise = (async () => {
|
|
63
|
+
const server = new TestServer({
|
|
64
|
+
dev: true,
|
|
65
|
+
enableSessions: true,
|
|
66
|
+
port: 0,
|
|
67
|
+
ssrRoot: frontDir,
|
|
68
|
+
initScript: path.join(serverDir, 'init.js'),
|
|
69
|
+
services: appConfig
|
|
70
|
+
})
|
|
71
|
+
console.log('starting test server')
|
|
72
|
+
try {
|
|
73
|
+
await server.start()
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error((error as Error).stack)
|
|
76
|
+
process.exit(1)
|
|
77
|
+
}
|
|
78
|
+
console.log('Test server started at', server.url)
|
|
79
|
+
console.log('waiting for front to be ready...')
|
|
80
|
+
await waitForServerReady(server.url!)
|
|
81
|
+
console.log('front ready')
|
|
82
|
+
|
|
83
|
+
process.on('beforeExit', () => {
|
|
84
|
+
server.dispose()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// When running under node:test, register a global teardown that disposes the server
|
|
88
|
+
// and forces process exit so the test run finishes cleanly.
|
|
89
|
+
try {
|
|
90
|
+
const { after } = await import('node:test')
|
|
91
|
+
after(async () => {
|
|
92
|
+
await server.dispose()
|
|
93
|
+
process.exit(0)
|
|
94
|
+
})
|
|
95
|
+
} catch {
|
|
96
|
+
// not running under node:test, ignore
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const url = server.url!
|
|
100
|
+
return {
|
|
101
|
+
server,
|
|
102
|
+
url,
|
|
103
|
+
haveService: (name: string) => haveService(server, name),
|
|
104
|
+
haveModel: (serviceName: string, modelName: string) => haveModel(server, serviceName, modelName),
|
|
105
|
+
haveView: (serviceName: string, viewName: string) => {
|
|
106
|
+
const service = haveService(server, serviceName)
|
|
107
|
+
const view = service.views[viewName]
|
|
108
|
+
if (!view) throw new Error('view ' + viewName + ' not found')
|
|
109
|
+
return view
|
|
110
|
+
},
|
|
111
|
+
haveAction: (serviceName: string, actionName: string) => {
|
|
112
|
+
const service = haveService(server, serviceName)
|
|
113
|
+
const action = service.actions[actionName]
|
|
114
|
+
if (!action) throw new Error('action ' + actionName + ' not found')
|
|
115
|
+
return action
|
|
116
|
+
},
|
|
117
|
+
haveTrigger: (serviceName: string, triggerName: string) => {
|
|
118
|
+
const service = haveService(server, serviceName)
|
|
119
|
+
const trigger = service.triggers[triggerName]
|
|
120
|
+
if (!trigger) throw new Error('trigger ' + triggerName + ' not found')
|
|
121
|
+
return trigger
|
|
122
|
+
},
|
|
123
|
+
grabObject: async (serviceName: string, modelName: string, id: string) => {
|
|
124
|
+
const model = haveModel(server, serviceName, modelName)
|
|
125
|
+
return await model.get(id)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
})()
|
|
129
|
+
return envPromise
|
|
130
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import App from '@live-change/framework'
|
|
4
|
+
import randomProfile from 'random-profile-generator'
|
|
5
|
+
import passwordGenerator from 'generate-password'
|
|
6
|
+
import { withBrowser } from './withBrowser.js'
|
|
7
|
+
import { useSecretCode } from './steps.js'
|
|
8
|
+
|
|
9
|
+
const app = App.app()
|
|
10
|
+
const email = randomProfile.profile().firstName.toLowerCase() + '@test.com'
|
|
11
|
+
|
|
12
|
+
test('reset password with email code', async () => {
|
|
13
|
+
await withBrowser(async (page, env) => {
|
|
14
|
+
const user = app.generateUid()
|
|
15
|
+
|
|
16
|
+
const User = env.haveModel('user', 'User')
|
|
17
|
+
const Email = env.haveModel('email', 'Email')
|
|
18
|
+
|
|
19
|
+
await User.create({ id: user, roles: [] })
|
|
20
|
+
await Email.create({ id: email, email, user })
|
|
21
|
+
await page.goto(env.url + '/', { waitUntil: 'networkidle' })
|
|
22
|
+
|
|
23
|
+
await page.goto(env.url + '/user/reset-password', { waitUntil: 'networkidle' })
|
|
24
|
+
await page.fill('input#email', email)
|
|
25
|
+
await page.click('button[type=submit]')
|
|
26
|
+
await page.waitForURL('**/sent/*', { timeout: 10000 })
|
|
27
|
+
|
|
28
|
+
assert.ok(page.url().includes('/sent'))
|
|
29
|
+
let url = page.url()
|
|
30
|
+
const authentication = url.split('/').pop()!
|
|
31
|
+
|
|
32
|
+
const authenticationData = await env.grabObject('messageAuthentication', 'Authentication', authentication)
|
|
33
|
+
assert.ok(authenticationData, 'authentication created')
|
|
34
|
+
assert.strictEqual((authenticationData as { messageData?: { user: string } })?.messageData?.user, user, 'authentication message data contains user')
|
|
35
|
+
assert.strictEqual((authenticationData as { actionProperties?: { user: string } })?.actionProperties?.user, user, 'authentication action properties contains user')
|
|
36
|
+
|
|
37
|
+
await useSecretCode(page, env, authentication, false)
|
|
38
|
+
await page.waitForURL('**/set-new-password/*', { timeout: 10000 })
|
|
39
|
+
|
|
40
|
+
assert.ok(page.url().includes('/set-new-password/'))
|
|
41
|
+
url = page.url()
|
|
42
|
+
const resetPasswordAuthentication = url.split('/').pop()!
|
|
43
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
44
|
+
const ResetPasswordAuthentication = env.haveModel('passwordAuthentication', 'ResetPasswordAuthentication') as {
|
|
45
|
+
indexObjectGet: (index: string, key: string) => Promise<unknown>
|
|
46
|
+
}
|
|
47
|
+
const resetPasswordAuthenticationData = await ResetPasswordAuthentication.indexObjectGet('byKey', resetPasswordAuthentication)
|
|
48
|
+
assert.ok(resetPasswordAuthenticationData, 'reset password authentication created')
|
|
49
|
+
|
|
50
|
+
await page.getByText('Reset password').waitFor({ state: 'visible' })
|
|
51
|
+
|
|
52
|
+
const password =
|
|
53
|
+
passwordGenerator.generate({ length: 10, numbers: true }) + (Math.random() * 10).toFixed()
|
|
54
|
+
await page.locator('input[type="password"]').nth(0).fill(password)
|
|
55
|
+
await page.locator('input[type="password"]').nth(1).fill(password)
|
|
56
|
+
await page.click('button[type=submit]')
|
|
57
|
+
await page.waitForURL('**/reset-password-finished', { timeout: 10000 })
|
|
58
|
+
assert.ok(page.url().includes('/reset-password-finished'))
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import App from '@live-change/framework'
|
|
4
|
+
import randomProfile from 'random-profile-generator'
|
|
5
|
+
import passwordGenerator from 'generate-password'
|
|
6
|
+
import { withBrowser } from './withBrowser.js'
|
|
7
|
+
import { useSecretLink } from './steps.js'
|
|
8
|
+
|
|
9
|
+
const app = App.app()
|
|
10
|
+
const email = randomProfile.profile().firstName.toLowerCase() + '@test.com'
|
|
11
|
+
|
|
12
|
+
test('reset password with email link', async () => {
|
|
13
|
+
await withBrowser(async (page, env) => {
|
|
14
|
+
const user = app.generateUid()
|
|
15
|
+
|
|
16
|
+
const User = env.haveModel('user', 'User')
|
|
17
|
+
const Email = env.haveModel('email', 'Email')
|
|
18
|
+
|
|
19
|
+
await User.create({ id: user, roles: [] })
|
|
20
|
+
await Email.create({ id: email, email, user })
|
|
21
|
+
await page.goto(env.url + '/', { waitUntil: 'networkidle' })
|
|
22
|
+
|
|
23
|
+
await page.goto(env.url + '/user/reset-password', { waitUntil: 'networkidle' })
|
|
24
|
+
await page.fill('input#email', email)
|
|
25
|
+
await page.click('button[type=submit]')
|
|
26
|
+
await page.waitForURL('**/sent/*', { timeout: 10000 })
|
|
27
|
+
|
|
28
|
+
assert.ok(page.url().includes('/sent'))
|
|
29
|
+
let url = page.url()
|
|
30
|
+
const authentication = url.split('/').pop()!
|
|
31
|
+
|
|
32
|
+
const authenticationData = await env.grabObject('messageAuthentication', 'Authentication', authentication)
|
|
33
|
+
assert.ok(authenticationData, 'authentication created')
|
|
34
|
+
assert.strictEqual((authenticationData as { messageData?: { user: string } })?.messageData?.user, user, 'authentication message data contains user')
|
|
35
|
+
assert.strictEqual((authenticationData as { actionProperties?: { user: string } })?.actionProperties?.user, user, 'authentication action properties contains user')
|
|
36
|
+
|
|
37
|
+
await useSecretLink(page, env, authentication, false)
|
|
38
|
+
await page.waitForURL('**/set-new-password/*', { timeout: 10000 })
|
|
39
|
+
|
|
40
|
+
assert.ok(page.url().includes('/set-new-password/'))
|
|
41
|
+
url = page.url()
|
|
42
|
+
const resetPasswordAuthentication = url.split('/').pop()!
|
|
43
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
44
|
+
const ResetPasswordAuthentication = env.haveModel('passwordAuthentication', 'ResetPasswordAuthentication') as {
|
|
45
|
+
indexObjectGet: (index: string, key: string) => Promise<unknown>
|
|
46
|
+
}
|
|
47
|
+
const resetPasswordAuthenticationData = await ResetPasswordAuthentication.indexObjectGet('byKey', resetPasswordAuthentication)
|
|
48
|
+
assert.ok(resetPasswordAuthenticationData, 'reset password authentication created')
|
|
49
|
+
|
|
50
|
+
await page.getByText('Reset password').waitFor({ state: 'visible' })
|
|
51
|
+
|
|
52
|
+
const password =
|
|
53
|
+
passwordGenerator.generate({ length: 10, numbers: true }) + (Math.random() * 10).toFixed()
|
|
54
|
+
await page.locator('input[type="password"]').nth(0).fill(password)
|
|
55
|
+
await page.locator('input[type="password"]').nth(1).fill(password)
|
|
56
|
+
await page.click('button[type=submit]')
|
|
57
|
+
await page.waitForURL('**/reset-password-finished', { timeout: 10000 })
|
|
58
|
+
assert.ok(page.url().includes('/reset-password-finished'))
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import App from '@live-change/framework'
|
|
4
|
+
import randomProfile from 'random-profile-generator'
|
|
5
|
+
import passwordGenerator from 'generate-password'
|
|
6
|
+
import { withBrowser } from './withBrowser.js'
|
|
7
|
+
|
|
8
|
+
const app = App.app()
|
|
9
|
+
const name = randomProfile.profile().firstName.toLowerCase()
|
|
10
|
+
const email = name + '@test.com'
|
|
11
|
+
|
|
12
|
+
test('set password', async () => {
|
|
13
|
+
await withBrowser(async (page, env) => {
|
|
14
|
+
const user = app.generateUid()
|
|
15
|
+
const User = env.haveModel('user', 'User')
|
|
16
|
+
const Email = env.haveModel('email', 'Email')
|
|
17
|
+
const AuthenticatedUser = env.haveModel('user', 'AuthenticatedUser')
|
|
18
|
+
const PasswordAuthentication = env.haveModel('passwordAuthentication', 'PasswordAuthentication')
|
|
19
|
+
|
|
20
|
+
await User.create({ id: user, roles: [] })
|
|
21
|
+
await Email.create({ id: email, email, user })
|
|
22
|
+
await page.goto(env.url + '/', { waitUntil: 'networkidle' })
|
|
23
|
+
const session = await page.evaluate(
|
|
24
|
+
() => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
|
|
25
|
+
)
|
|
26
|
+
await AuthenticatedUser.create({ id: session, user, session })
|
|
27
|
+
|
|
28
|
+
await page.reload({ waitUntil: 'networkidle' })
|
|
29
|
+
const clientUser = await page.evaluate(
|
|
30
|
+
() => (window as unknown as { api: { client: { value: { user: string } } } }).api.client.value.user
|
|
31
|
+
)
|
|
32
|
+
assert.strictEqual(user, clientUser, 'client logged in')
|
|
33
|
+
|
|
34
|
+
const emptyPasswordAuthenticationData = await PasswordAuthentication.get(user)
|
|
35
|
+
assert.strictEqual(emptyPasswordAuthenticationData, null, 'password not set')
|
|
36
|
+
|
|
37
|
+
await page.goto(env.url + '/user/settings/change-password', { waitUntil: 'networkidle' })
|
|
38
|
+
await page.getByText('Set password').waitFor({ state: 'visible' })
|
|
39
|
+
|
|
40
|
+
const firstPassword =
|
|
41
|
+
passwordGenerator.generate({ length: 10, numbers: true }) + (Math.random() * 10).toFixed()
|
|
42
|
+
await page.locator('input[type="password"]').nth(0).fill(firstPassword)
|
|
43
|
+
await page.locator('input[type="password"]').nth(1).fill(firstPassword)
|
|
44
|
+
await page.click('button[type=submit]')
|
|
45
|
+
assert.ok(page.url().includes('/user/settings/change-password-finished'))
|
|
46
|
+
|
|
47
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
48
|
+
const firstPasswordAuthenticationData = await PasswordAuthentication.get(user)
|
|
49
|
+
assert.ok(firstPasswordAuthenticationData, 'password set')
|
|
50
|
+
|
|
51
|
+
await page.goto(env.url + '/user/settings/change-password', { waitUntil: 'networkidle' })
|
|
52
|
+
await page.getByText('Change password').waitFor({ state: 'visible' })
|
|
53
|
+
|
|
54
|
+
const secondPassword =
|
|
55
|
+
passwordGenerator.generate({ length: 10, numbers: true }) + (Math.random() * 10).toFixed()
|
|
56
|
+
await page.locator('input[type="password"]').nth(0).fill(firstPassword)
|
|
57
|
+
await page.locator('input[type="password"]').nth(1).fill(secondPassword)
|
|
58
|
+
await page.locator('input[type="password"]').nth(2).fill(secondPassword)
|
|
59
|
+
await page.click('button[type=submit]')
|
|
60
|
+
|
|
61
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
62
|
+
const secondPasswordAuthenticationData = await PasswordAuthentication.get(user)
|
|
63
|
+
assert.ok(secondPasswordAuthenticationData, 'password set')
|
|
64
|
+
assert.notStrictEqual(
|
|
65
|
+
(secondPasswordAuthenticationData as { passwordHash: string }).passwordHash,
|
|
66
|
+
(firstPasswordAuthenticationData as { passwordHash: string }).passwordHash,
|
|
67
|
+
'password changed'
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import App from '@live-change/framework'
|
|
4
|
+
import randomProfile from 'random-profile-generator'
|
|
5
|
+
import { withBrowser } from './withBrowser.js'
|
|
6
|
+
import { useSecretCode } from './steps.js'
|
|
7
|
+
|
|
8
|
+
const app = App.app()
|
|
9
|
+
const randomUserData = randomProfile.profile()
|
|
10
|
+
;(randomUserData as { email?: string }).email =
|
|
11
|
+
(randomUserData as { firstName: string }).firstName.toLowerCase() + '@test.com'
|
|
12
|
+
const happyPath = false
|
|
13
|
+
|
|
14
|
+
test('sign in with email code', async () => {
|
|
15
|
+
await withBrowser(async (page, env) => {
|
|
16
|
+
const user = app.generateUid()
|
|
17
|
+
const email = (randomUserData as { email: string }).email
|
|
18
|
+
|
|
19
|
+
const User = env.haveModel('user', 'User')
|
|
20
|
+
const Email = env.haveModel('email', 'Email')
|
|
21
|
+
|
|
22
|
+
await User.create({ id: user, roles: [] })
|
|
23
|
+
await Email.create({ id: email, email, user })
|
|
24
|
+
|
|
25
|
+
await page.goto(env.url + '/user/sign-in-email', { waitUntil: 'networkidle' })
|
|
26
|
+
await page.fill('input#email', email)
|
|
27
|
+
await page.click('button[type=submit]')
|
|
28
|
+
await page.waitForURL('**/sent/*', { timeout: 10000 })
|
|
29
|
+
|
|
30
|
+
assert.ok(page.url().includes('/sent/'))
|
|
31
|
+
const url = page.url()
|
|
32
|
+
const authentication = url.split('/').pop()!
|
|
33
|
+
|
|
34
|
+
const authenticationData = await env.grabObject('messageAuthentication', 'Authentication', authentication)
|
|
35
|
+
assert.ok(authenticationData, 'authentication created')
|
|
36
|
+
assert.strictEqual((authenticationData as { messageData?: { user: string } })?.messageData?.user, user, 'authentication contains user')
|
|
37
|
+
|
|
38
|
+
await useSecretCode(page, env, authentication, happyPath)
|
|
39
|
+
await page.waitForURL('**/sign-in-finished', { timeout: 10000 })
|
|
40
|
+
|
|
41
|
+
assert.ok(page.url().includes('/user/sign-in-finished'))
|
|
42
|
+
const clientSession = await page.evaluate(
|
|
43
|
+
() => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
|
|
44
|
+
)
|
|
45
|
+
const AuthenticatedUser = env.haveModel('user', 'AuthenticatedUser')
|
|
46
|
+
const authenticatedUserData = await AuthenticatedUser.get(clientSession)
|
|
47
|
+
assert.ok(authenticatedUserData, 'user authenticated server-side')
|
|
48
|
+
const clientUser = await page.evaluate(
|
|
49
|
+
() => (window as unknown as { api: { client: { value: { user: string } } } }).api.client.value.user
|
|
50
|
+
)
|
|
51
|
+
assert.strictEqual(clientUser, (authenticatedUserData as { user: string }).user, 'user authenticated')
|
|
52
|
+
|
|
53
|
+
if (!happyPath) {
|
|
54
|
+
await page.goto(url, { waitUntil: 'networkidle' })
|
|
55
|
+
assert.ok(page.url().includes('/user/sign-in-finished'))
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import App from '@live-change/framework'
|
|
4
|
+
import randomProfile from 'random-profile-generator'
|
|
5
|
+
import { withBrowser } from './withBrowser.js'
|
|
6
|
+
import { useSecretLink } from './steps.js'
|
|
7
|
+
|
|
8
|
+
const app = App.app()
|
|
9
|
+
const randomUserData = randomProfile.profile()
|
|
10
|
+
;(randomUserData as { email?: string }).email =
|
|
11
|
+
(randomUserData as { firstName: string }).firstName.toLowerCase() + '@test.com'
|
|
12
|
+
const happyPath = false
|
|
13
|
+
|
|
14
|
+
test('sign in with email link', async () => {
|
|
15
|
+
await withBrowser(async (page, env) => {
|
|
16
|
+
const user = app.generateUid()
|
|
17
|
+
const email = (randomUserData as { email: string }).email
|
|
18
|
+
|
|
19
|
+
const User = env.haveModel('user', 'User')
|
|
20
|
+
const Email = env.haveModel('email', 'Email')
|
|
21
|
+
|
|
22
|
+
await User.create({ id: user, roles: [] })
|
|
23
|
+
await Email.create({ id: email, email, user })
|
|
24
|
+
|
|
25
|
+
await page.goto(env.url + '/user/sign-in-email', { waitUntil: 'networkidle' })
|
|
26
|
+
await page.fill('input#email', email)
|
|
27
|
+
await page.click('button[type=submit]')
|
|
28
|
+
await page.waitForURL('**/sent/*', { timeout: 10000 })
|
|
29
|
+
|
|
30
|
+
assert.ok(page.url().includes('/sent/'))
|
|
31
|
+
const url = page.url()
|
|
32
|
+
const authentication = url.split('/').pop()!
|
|
33
|
+
|
|
34
|
+
const authenticationData = await env.grabObject('messageAuthentication', 'Authentication', authentication)
|
|
35
|
+
assert.ok(authenticationData, 'authentication created')
|
|
36
|
+
assert.strictEqual((authenticationData as { messageData?: { user: string } })?.messageData?.user, user, 'authentication contains user')
|
|
37
|
+
|
|
38
|
+
const linkData = await useSecretLink(page, env, authentication, happyPath)
|
|
39
|
+
await page.waitForURL('**/sign-in-finished', { timeout: 10000 })
|
|
40
|
+
|
|
41
|
+
assert.ok(page.url().includes('/user/sign-in-finished'))
|
|
42
|
+
const clientSession = await page.evaluate(
|
|
43
|
+
() => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
|
|
44
|
+
)
|
|
45
|
+
const AuthenticatedUser = env.haveModel('user', 'AuthenticatedUser')
|
|
46
|
+
const authenticatedUserData = await AuthenticatedUser.get(clientSession)
|
|
47
|
+
assert.ok(authenticatedUserData, 'user authenticated server-side')
|
|
48
|
+
const clientUser = await page.evaluate(
|
|
49
|
+
() => (window as unknown as { api: { client: { value: { user: string } } } }).api.client.value.user
|
|
50
|
+
)
|
|
51
|
+
assert.strictEqual(clientUser, (authenticatedUserData as { user: string }).user, 'user authenticated')
|
|
52
|
+
|
|
53
|
+
if (!happyPath) {
|
|
54
|
+
await page.goto(env.url + '/user/link/' + linkData.secretCode, { waitUntil: 'networkidle' })
|
|
55
|
+
await page.getByText('Link used').waitFor({ state: 'visible' })
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
})
|