@live-change/user-frontend 0.9.196 → 0.9.198

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.
@@ -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
+ })