@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,51 @@
|
|
|
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 crypto from 'crypto'
|
|
6
|
+
import passwordGenerator from 'generate-password'
|
|
7
|
+
import { withBrowser } from './withBrowser.js'
|
|
8
|
+
|
|
9
|
+
const app = App.app()
|
|
10
|
+
const randomUserData = randomProfile.profile()
|
|
11
|
+
;(randomUserData as { email?: string }).email =
|
|
12
|
+
(randomUserData as { firstName: string }).firstName.toLowerCase() + '@test.com'
|
|
13
|
+
|
|
14
|
+
test('sign in with email and password', async () => {
|
|
15
|
+
await withBrowser(async (page, env) => {
|
|
16
|
+
const user = app.generateUid()
|
|
17
|
+
const email = (randomUserData as { email: string }).email
|
|
18
|
+
const password =
|
|
19
|
+
passwordGenerator.generate({ length: 10, numbers: true }) + (Math.random() * 10).toFixed()
|
|
20
|
+
const passwordHash = crypto.createHash('sha256').update(password).digest('hex')
|
|
21
|
+
|
|
22
|
+
const User = env.haveModel('user', 'User')
|
|
23
|
+
const Email = env.haveModel('email', 'Email')
|
|
24
|
+
const PasswordAuthentication = env.haveModel('passwordAuthentication', 'PasswordAuthentication')
|
|
25
|
+
|
|
26
|
+
await User.create({ id: user, roles: [] })
|
|
27
|
+
await Email.create({ id: email, email, user })
|
|
28
|
+
await PasswordAuthentication.create({ id: user, user, passwordHash })
|
|
29
|
+
|
|
30
|
+
await page.goto(env.url + '/user/sign-in-email', { waitUntil: 'networkidle' })
|
|
31
|
+
await page.fill('input#email', email)
|
|
32
|
+
await page.fill('input[type="password"]', password)
|
|
33
|
+
await page.click('button[type=submit]')
|
|
34
|
+
await page.waitForURL('**/sign-in-finished', { timeout: 10000 })
|
|
35
|
+
|
|
36
|
+
assert.ok(page.url().includes('/user/sign-in-finished'))
|
|
37
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
38
|
+
|
|
39
|
+
const clientSession = await page.evaluate(
|
|
40
|
+
() => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
|
|
41
|
+
)
|
|
42
|
+
const AuthenticatedUser = env.haveModel('user', 'AuthenticatedUser')
|
|
43
|
+
const authenticatedUserData = await AuthenticatedUser.get(clientSession)
|
|
44
|
+
assert.ok(authenticatedUserData, 'user authenticated server-side')
|
|
45
|
+
|
|
46
|
+
const clientUser = await page.evaluate(
|
|
47
|
+
() => (window as unknown as { api: { client: { value: { user: string } } } }).api.client.value.user
|
|
48
|
+
)
|
|
49
|
+
assert.strictEqual(clientUser, (authenticatedUserData as { user: string }).user, 'user authenticated')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
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('sign out', async () => {
|
|
12
|
+
await withBrowser(async (page, env) => {
|
|
13
|
+
const user = app.generateUid()
|
|
14
|
+
const User = env.haveModel('user', 'User')
|
|
15
|
+
const Email = env.haveModel('email', 'Email')
|
|
16
|
+
const AuthenticatedUser = env.haveModel('user', 'AuthenticatedUser')
|
|
17
|
+
|
|
18
|
+
await User.create({ id: user, roles: [] })
|
|
19
|
+
await Email.create({ id: email, email, user })
|
|
20
|
+
await page.goto(env.url + '/', { waitUntil: 'networkidle' })
|
|
21
|
+
const session = await page.evaluate(
|
|
22
|
+
() => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
|
|
23
|
+
)
|
|
24
|
+
await AuthenticatedUser.create({ id: session, user, session })
|
|
25
|
+
|
|
26
|
+
await page.reload({ waitUntil: 'networkidle' })
|
|
27
|
+
const clientUser = await page.evaluate(
|
|
28
|
+
() => (window as unknown as { api: { client: { value: { user: string } } } }).api.client.value.user
|
|
29
|
+
)
|
|
30
|
+
assert.strictEqual(user, clientUser, 'client logged in')
|
|
31
|
+
|
|
32
|
+
await page.goto(env.url + '/user/sign-out', { waitUntil: 'networkidle' })
|
|
33
|
+
await page.waitForURL('**/sign-out-finished', { timeout: 10000 })
|
|
34
|
+
assert.ok(page.url().includes('/user/sign-out-finished'))
|
|
35
|
+
|
|
36
|
+
await new Promise((r) => setTimeout(r, 1000))
|
|
37
|
+
const clientUser2 = await page.evaluate(
|
|
38
|
+
() => (window as unknown as { api: { client: { value: { user: unknown } } } }).api.client.value.user
|
|
39
|
+
)
|
|
40
|
+
const authenticatedUserData = await AuthenticatedUser.get(session)
|
|
41
|
+
|
|
42
|
+
assert.strictEqual(!!authenticatedUserData, false, 'no server user')
|
|
43
|
+
assert.strictEqual(!!clientUser2, false, 'no client user')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import randomProfile from 'random-profile-generator'
|
|
4
|
+
import { withBrowser } from './withBrowser.js'
|
|
5
|
+
import { useSecretCode } from './steps.js'
|
|
6
|
+
|
|
7
|
+
const user = randomProfile.profile()
|
|
8
|
+
;(user as { email?: string }).email =
|
|
9
|
+
(user as { firstName: string }).firstName.toLowerCase() + '@test.com'
|
|
10
|
+
const happyPath = false
|
|
11
|
+
|
|
12
|
+
test('sign up with email code', async () => {
|
|
13
|
+
await withBrowser(async (page, env) => {
|
|
14
|
+
await page.goto(env.url + '/user/sign-up-email', { waitUntil: 'networkidle' })
|
|
15
|
+
await page.fill('input#email', (user as { email: string }).email)
|
|
16
|
+
await page.click('button[type=submit]')
|
|
17
|
+
await page.waitForURL('**/sent/*', { timeout: 10000 })
|
|
18
|
+
|
|
19
|
+
assert.ok(page.url().includes('/sent/'))
|
|
20
|
+
const url = page.url()
|
|
21
|
+
const authentication = url.split('/').pop()!
|
|
22
|
+
|
|
23
|
+
const authenticationData = await env.grabObject('messageAuthentication', 'Authentication', authentication)
|
|
24
|
+
assert.ok(authenticationData, 'authentication created')
|
|
25
|
+
|
|
26
|
+
await useSecretCode(page, env, authentication, happyPath)
|
|
27
|
+
await page.waitForURL('**/sign-up-finished', { timeout: 10000 })
|
|
28
|
+
|
|
29
|
+
assert.ok(page.url().includes('/user/sign-up-finished'))
|
|
30
|
+
const clientSession = await page.evaluate(
|
|
31
|
+
() => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
|
|
32
|
+
)
|
|
33
|
+
const AuthenticatedUser = env.haveModel('user', 'AuthenticatedUser')
|
|
34
|
+
const authenticatedUserData = await AuthenticatedUser.get(clientSession)
|
|
35
|
+
assert.ok(authenticatedUserData, 'user authenticated server-side')
|
|
36
|
+
const clientUser = await page.evaluate(
|
|
37
|
+
() => (window as unknown as { api: { client: { value: { user: string } } } }).api.client.value.user
|
|
38
|
+
)
|
|
39
|
+
assert.strictEqual(clientUser, (authenticatedUserData as { user: string }).user, 'user authenticated')
|
|
40
|
+
|
|
41
|
+
if (!happyPath) {
|
|
42
|
+
await page.goto(url, { waitUntil: 'networkidle' })
|
|
43
|
+
assert.ok(page.url().includes('/user/sign-up-finished'))
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import randomProfile from 'random-profile-generator'
|
|
4
|
+
import { withBrowser } from './withBrowser.js'
|
|
5
|
+
import { useSecretLink } from './steps.js'
|
|
6
|
+
|
|
7
|
+
const user = randomProfile.profile()
|
|
8
|
+
;(user as { email?: string }).email =
|
|
9
|
+
(user as { firstName: string }).firstName.toLowerCase() + '@test.com'
|
|
10
|
+
const happyPath = false
|
|
11
|
+
|
|
12
|
+
test('sign up with email link', async () => {
|
|
13
|
+
await withBrowser(async (page, env) => {
|
|
14
|
+
await page.goto(env.url + '/user/sign-up-email', { waitUntil: 'networkidle' })
|
|
15
|
+
await page.fill('input#email', (user as { email: string }).email)
|
|
16
|
+
await page.click('button[type=submit]')
|
|
17
|
+
await page.waitForURL('**/sent/*', { timeout: 10000 })
|
|
18
|
+
|
|
19
|
+
assert.ok(page.url().includes('/sent/'))
|
|
20
|
+
const url = page.url()
|
|
21
|
+
const authentication = url.split('/').pop()!
|
|
22
|
+
|
|
23
|
+
const authenticationData = await env.grabObject('messageAuthentication', 'Authentication', authentication)
|
|
24
|
+
assert.ok(authenticationData, 'authentication created')
|
|
25
|
+
|
|
26
|
+
const linkData = await useSecretLink(page, env, authentication, happyPath)
|
|
27
|
+
await page.waitForURL('**/sign-up-finished', { timeout: 10000 })
|
|
28
|
+
|
|
29
|
+
assert.ok(page.url().includes('/user/sign-up-finished'))
|
|
30
|
+
const clientSession = await page.evaluate(
|
|
31
|
+
() => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
|
|
32
|
+
)
|
|
33
|
+
const AuthenticatedUser = env.haveModel('user', 'AuthenticatedUser')
|
|
34
|
+
const authenticatedUserData = await AuthenticatedUser.get(clientSession)
|
|
35
|
+
assert.ok(authenticatedUserData, 'user authenticated server-side')
|
|
36
|
+
const clientUser = await page.evaluate(
|
|
37
|
+
() => (window as unknown as { api: { client: { value: { user: string } } } }).api.client.value.user
|
|
38
|
+
)
|
|
39
|
+
assert.strictEqual(clientUser, (authenticatedUserData as { user: string }).user, 'user authenticated')
|
|
40
|
+
|
|
41
|
+
if (!happyPath) {
|
|
42
|
+
await page.goto(env.url + '/user/link/' + linkData.secretCode, { waitUntil: 'networkidle' })
|
|
43
|
+
await page.getByText('Link used').waitFor({ state: 'visible' })
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
})
|
package/e2e/steps.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import type { Page } from 'playwright'
|
|
3
|
+
import App from '@live-change/framework'
|
|
4
|
+
import randomProfile from 'random-profile-generator'
|
|
5
|
+
import passwordGenerator from 'generate-password'
|
|
6
|
+
import type { TestEnv } from './env.js'
|
|
7
|
+
|
|
8
|
+
const app = App.app()
|
|
9
|
+
|
|
10
|
+
export async function haveUser(
|
|
11
|
+
env: TestEnv,
|
|
12
|
+
name?: string,
|
|
13
|
+
email?: string,
|
|
14
|
+
password?: string,
|
|
15
|
+
user: string = app.generateUid(),
|
|
16
|
+
roles: string[] = []
|
|
17
|
+
): Promise<{ id: string; name: string; email: string; password: string }> {
|
|
18
|
+
if (!password) password = passwordGenerator.generate({ length: 10, numbers: true })
|
|
19
|
+
if (!name) name = randomProfile.profile().firstName
|
|
20
|
+
if (!email) {
|
|
21
|
+
name = name!
|
|
22
|
+
email = name.split(' ')[0].toLowerCase() + (Math.random() * 100).toFixed() + '@test.com'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const PasswordAuthentication = env.haveModel('passwordAuthentication', 'PasswordAuthentication')
|
|
26
|
+
const User = env.haveModel('user', 'User')
|
|
27
|
+
const Email = env.haveModel('email', 'Email')
|
|
28
|
+
const Identification = env.haveModel('userIdentification', 'Identification')
|
|
29
|
+
|
|
30
|
+
const passwordHash = PasswordAuthentication.definition.properties.passwordHash.preFilter(password)
|
|
31
|
+
await User.create({ id: user, roles })
|
|
32
|
+
await PasswordAuthentication.create({ id: user, user, passwordHash })
|
|
33
|
+
await Email.create({ id: email, email, user })
|
|
34
|
+
await Identification.create({
|
|
35
|
+
id: App.encodeIdentifier(['user_User', user]),
|
|
36
|
+
sessionOrUserType: 'user_User',
|
|
37
|
+
sessionOrUser: user,
|
|
38
|
+
name: name!
|
|
39
|
+
})
|
|
40
|
+
return { id: user, name: name!, email, password }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function useEmailLink(
|
|
44
|
+
page: Page,
|
|
45
|
+
env: TestEnv,
|
|
46
|
+
email: string,
|
|
47
|
+
prefix = '/user/link/'
|
|
48
|
+
): Promise<{ authentication: unknown; link: { secretCode: string } }> {
|
|
49
|
+
const MessageAuthentication = env.haveModel('messageAuthentication', 'Authentication') as {
|
|
50
|
+
indexObjectGet: (index: string, key: unknown[], opts?: { reverse?: boolean; limit?: number }) => Promise<unknown>
|
|
51
|
+
}
|
|
52
|
+
const authentication = await MessageAuthentication.indexObjectGet('byContact', ['email', email], {
|
|
53
|
+
reverse: true,
|
|
54
|
+
limit: 1
|
|
55
|
+
})
|
|
56
|
+
const Link = env.haveModel('secretLink', 'Link') as {
|
|
57
|
+
indexObjectGet: (index: string, key: unknown) => Promise<{ secretCode: string }>
|
|
58
|
+
}
|
|
59
|
+
const link = await Link.indexObjectGet('byAuthentication', authentication)
|
|
60
|
+
await page.goto(env.url + prefix + link.secretCode, { waitUntil: 'networkidle' })
|
|
61
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
62
|
+
return { authentication, link }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function amLoggedIn(page: Page, env: TestEnv, user: { id: string }): Promise<void> {
|
|
66
|
+
const AuthenticatedUser = env.haveModel('user', 'AuthenticatedUser')
|
|
67
|
+
const session = await page.evaluate(() => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session)
|
|
68
|
+
await AuthenticatedUser.create({ id: session, session, user: user.id })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function amLoggedOut(page: Page, env: TestEnv): Promise<void> {
|
|
72
|
+
const AuthenticatedUser = env.haveModel('user', 'AuthenticatedUser')
|
|
73
|
+
const session = await page.evaluate(() => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session)
|
|
74
|
+
await AuthenticatedUser.delete(session)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function useSecretCode(
|
|
78
|
+
page: Page,
|
|
79
|
+
env: TestEnv,
|
|
80
|
+
authentication: unknown,
|
|
81
|
+
happyPath: boolean
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
const Code = env.haveModel('secretCode', 'Code') as {
|
|
84
|
+
indexObjectGet: (index: string, key: unknown) => Promise<{ id: string; secretCode: string; expire: Date }>
|
|
85
|
+
indexRangeGet: (index: string, key: unknown) => Promise<{ id: string; secretCode: string; expire: Date }[]>
|
|
86
|
+
update: (id: string, data: { expire: Date }) => Promise<unknown>
|
|
87
|
+
}
|
|
88
|
+
let codeData = await Code.indexObjectGet('byAuthentication', authentication)
|
|
89
|
+
assert.ok(codeData, 'code created')
|
|
90
|
+
|
|
91
|
+
if (!happyPath) {
|
|
92
|
+
await sleep(200)
|
|
93
|
+
await page.waitForSelector('input#code', { state: 'visible' })
|
|
94
|
+
const wrongCode = codeData!.secretCode === '123456' ? '654321' : '123456'
|
|
95
|
+
await page.fill('input#code', wrongCode)
|
|
96
|
+
await page.fill('input#code', wrongCode)
|
|
97
|
+
await page.click('button[type=submit]')
|
|
98
|
+
// PrimeVue 4 uses <Message> with role=\"alert\" instead of a #code-help.p-error span
|
|
99
|
+
await page.getByRole('alert').waitFor({ state: 'visible' })
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!happyPath) {
|
|
103
|
+
await sleep(200)
|
|
104
|
+
await page.waitForSelector('input#code', { state: 'visible' })
|
|
105
|
+
await Code.update(codeData!.id, { expire: new Date() })
|
|
106
|
+
await page.fill('input#code', codeData!.secretCode)
|
|
107
|
+
await page.fill('input#code', codeData!.secretCode)
|
|
108
|
+
await page.click('button[type=submit]')
|
|
109
|
+
await page.getByRole('alert').waitFor({ state: 'visible' })
|
|
110
|
+
|
|
111
|
+
await page.click('text=Resend')
|
|
112
|
+
assert.ok(page.url().includes('/sent/'))
|
|
113
|
+
|
|
114
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
115
|
+
const newCodeData = await Code.indexRangeGet('byAuthentication', authentication)
|
|
116
|
+
newCodeData.sort((a, b) => new Date(b.expire).getTime() - new Date(a.expire).getTime())
|
|
117
|
+
const oldCodeData = codeData
|
|
118
|
+
codeData = newCodeData[0]
|
|
119
|
+
assert.ok(codeData, 'code exists')
|
|
120
|
+
assert.notStrictEqual(oldCodeData!.id, codeData!.id, 'code is different from previous code')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await page.waitForFunction(() => {
|
|
124
|
+
const input = document.querySelector('input#code') as HTMLInputElement
|
|
125
|
+
if(!input) return false
|
|
126
|
+
return input.value === ''
|
|
127
|
+
})
|
|
128
|
+
await page.fill('input#code', codeData!.secretCode)
|
|
129
|
+
await page.fill('input#code', codeData!.secretCode)
|
|
130
|
+
await page.click('button[type=submit]')
|
|
131
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function useSecretLink(
|
|
135
|
+
page: Page,
|
|
136
|
+
env: TestEnv,
|
|
137
|
+
authentication: unknown,
|
|
138
|
+
happyPath: boolean,
|
|
139
|
+
prefix = '/user'
|
|
140
|
+
): Promise<{ secretCode: string }> {
|
|
141
|
+
const Link = env.haveModel('secretLink', 'Link') as {
|
|
142
|
+
indexObjectGet: (index: string, key: unknown) => Promise<{ id: string; secretCode: string; expire: Date }>
|
|
143
|
+
indexRangeGet: (index: string, key: unknown) => Promise<{ id: string; secretCode: string; expire: Date }[]>
|
|
144
|
+
update: (id: string, data: { expire: Date }) => Promise<unknown>
|
|
145
|
+
}
|
|
146
|
+
let linkData = await Link.indexObjectGet('byAuthentication', authentication)
|
|
147
|
+
assert.ok(linkData, 'link created')
|
|
148
|
+
|
|
149
|
+
if (!happyPath) {
|
|
150
|
+
await page.goto(env.url + prefix + '/link/[badSecret]', { waitUntil: 'networkidle' })
|
|
151
|
+
await page.getByText('Unknown link').waitFor({ state: 'visible' })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!happyPath) {
|
|
155
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
156
|
+
await Link.update(linkData!.id, { expire: new Date() })
|
|
157
|
+
await page.goto(env.url + prefix + '/link/' + linkData!.secretCode, { waitUntil: 'networkidle' })
|
|
158
|
+
await page.getByText('Link expired').waitFor({ state: 'visible' })
|
|
159
|
+
|
|
160
|
+
await page.click('text=Resend')
|
|
161
|
+
assert.ok(page.url().includes(prefix + '/sent/'))
|
|
162
|
+
|
|
163
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
164
|
+
const newLinksData = await Link.indexRangeGet('byAuthentication', authentication)
|
|
165
|
+
newLinksData.sort((a, b) => new Date(b.expire).getTime() - new Date(a.expire).getTime())
|
|
166
|
+
const oldLinkData = linkData
|
|
167
|
+
linkData = newLinksData[0]
|
|
168
|
+
assert.ok(linkData, 'link exists')
|
|
169
|
+
assert.notStrictEqual(oldLinkData!.id, linkData!.id, 'link is different from previous link')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await page.goto(env.url + prefix + '/link/' + linkData!.secretCode, { waitUntil: 'networkidle' })
|
|
173
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
174
|
+
return linkData!
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function sleep(ms: number) {
|
|
178
|
+
return new Promise((r) => setTimeout(r, ms))
|
|
179
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { chromium } from 'playwright'
|
|
2
|
+
import { getTestEnv } from './env.js'
|
|
3
|
+
import type { TestEnv } from './env.js'
|
|
4
|
+
import type { Page } from 'playwright'
|
|
5
|
+
|
|
6
|
+
export async function withBrowser(
|
|
7
|
+
fn: (page: Page, env: TestEnv) => Promise<void>
|
|
8
|
+
): Promise<void> {
|
|
9
|
+
const env = await getTestEnv()
|
|
10
|
+
const browser = await chromium.launch({ headless: false })
|
|
11
|
+
const context = await browser.newContext()
|
|
12
|
+
const page = await context.newPage()
|
|
13
|
+
try {
|
|
14
|
+
await fn(page, env)
|
|
15
|
+
} finally {
|
|
16
|
+
await context.close()
|
|
17
|
+
await browser.close()
|
|
18
|
+
}
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@live-change/user-frontend",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.197",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"memDev": "tsx --inspect --expose-gc server/start.js memDev --enableSessions --initScript ./init.js --dbAccess",
|
|
6
6
|
"localDevInit": "tsx server/start.js localDev --enableSessions --initScript ./init.js --dbAccess",
|
|
@@ -36,29 +36,29 @@
|
|
|
36
36
|
},
|
|
37
37
|
"type": "module",
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@live-change/cli": "^0.9.
|
|
40
|
-
"@live-change/dao": "^0.9.
|
|
41
|
-
"@live-change/dao-vue3": "^0.9.
|
|
42
|
-
"@live-change/dao-websocket": "^0.9.
|
|
43
|
-
"@live-change/email-service": "^0.9.
|
|
44
|
-
"@live-change/framework": "^0.9.
|
|
45
|
-
"@live-change/identicon-service": "^0.9.
|
|
46
|
-
"@live-change/image-frontend": "^0.9.
|
|
47
|
-
"@live-change/message-authentication-service": "^0.9.
|
|
48
|
-
"@live-change/notification-service": "^0.9.
|
|
49
|
-
"@live-change/password-authentication-service": "^0.9.
|
|
50
|
-
"@live-change/pattern": "^0.9.
|
|
51
|
-
"@live-change/secret-code-service": "^0.9.
|
|
52
|
-
"@live-change/secret-link-service": "^0.9.
|
|
53
|
-
"@live-change/security-frontend": "^0.9.
|
|
54
|
-
"@live-change/security-service": "^0.9.
|
|
55
|
-
"@live-change/session-service": "^0.9.
|
|
56
|
-
"@live-change/timer-service": "^0.9.
|
|
57
|
-
"@live-change/upload-service": "^0.9.
|
|
58
|
-
"@live-change/user-identification-service": "^0.9.
|
|
59
|
-
"@live-change/user-service": "^0.9.
|
|
60
|
-
"@live-change/vue3-components": "^0.9.
|
|
61
|
-
"@live-change/vue3-ssr": "^0.9.
|
|
39
|
+
"@live-change/cli": "^0.9.197",
|
|
40
|
+
"@live-change/dao": "^0.9.197",
|
|
41
|
+
"@live-change/dao-vue3": "^0.9.197",
|
|
42
|
+
"@live-change/dao-websocket": "^0.9.197",
|
|
43
|
+
"@live-change/email-service": "^0.9.197",
|
|
44
|
+
"@live-change/framework": "^0.9.197",
|
|
45
|
+
"@live-change/identicon-service": "^0.9.197",
|
|
46
|
+
"@live-change/image-frontend": "^0.9.197",
|
|
47
|
+
"@live-change/message-authentication-service": "^0.9.197",
|
|
48
|
+
"@live-change/notification-service": "^0.9.197",
|
|
49
|
+
"@live-change/password-authentication-service": "^0.9.197",
|
|
50
|
+
"@live-change/pattern": "^0.9.197",
|
|
51
|
+
"@live-change/secret-code-service": "^0.9.197",
|
|
52
|
+
"@live-change/secret-link-service": "^0.9.197",
|
|
53
|
+
"@live-change/security-frontend": "^0.9.197",
|
|
54
|
+
"@live-change/security-service": "^0.9.197",
|
|
55
|
+
"@live-change/session-service": "^0.9.197",
|
|
56
|
+
"@live-change/timer-service": "^0.9.197",
|
|
57
|
+
"@live-change/upload-service": "^0.9.197",
|
|
58
|
+
"@live-change/user-identification-service": "^0.9.197",
|
|
59
|
+
"@live-change/user-service": "^0.9.197",
|
|
60
|
+
"@live-change/vue3-components": "^0.9.197",
|
|
61
|
+
"@live-change/vue3-ssr": "^0.9.197",
|
|
62
62
|
"@vueuse/core": "^12.3.0",
|
|
63
63
|
"codeceptjs-assert": "^0.0.5",
|
|
64
64
|
"codeceptjs-video-helper": "0.1.3",
|
|
@@ -79,8 +79,8 @@
|
|
|
79
79
|
"wtfnode": "^0.9.1"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@live-change/codeceptjs-helper": "^0.9.
|
|
83
|
-
"codeceptjs": "^3.6
|
|
82
|
+
"@live-change/codeceptjs-helper": "^0.9.197",
|
|
83
|
+
"codeceptjs": "^3.7.6",
|
|
84
84
|
"generate-password": "1.7.1",
|
|
85
85
|
"playwright": "1.49.1",
|
|
86
86
|
"random-profile-generator": "^2.3.0",
|
|
@@ -90,5 +90,5 @@
|
|
|
90
90
|
"author": "Michał Łaszczewski <michal@laszczewski.pl>",
|
|
91
91
|
"license": "BSD-3-Clause",
|
|
92
92
|
"description": "",
|
|
93
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "8231c2ed8bc3beed2c732aa5727174417f19082b"
|
|
94
94
|
}
|
package/server/app.config.js
CHANGED
|
@@ -1,8 +1,35 @@
|
|
|
1
|
+
import dotenv from 'dotenv'
|
|
2
|
+
dotenv.config()
|
|
3
|
+
|
|
1
4
|
import App from "@live-change/framework"
|
|
2
5
|
const app = App.app()
|
|
3
6
|
|
|
4
|
-
import
|
|
5
|
-
|
|
7
|
+
import { fileURLToPath } from 'url'
|
|
8
|
+
import { dirname, join } from 'path'
|
|
9
|
+
import { accessSync, readFileSync } from 'fs'
|
|
10
|
+
|
|
11
|
+
const packageJsonPath = dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
.split('/').map((part, i, arr) =>
|
|
13
|
+
join(arr.slice(0, arr.length - i).join('/'), 'package.json')
|
|
14
|
+
).find(p => { try { accessSync(p); return true } catch(e) { return false }})
|
|
15
|
+
const packageJson = packageJsonPath ? JSON.parse(readFileSync(packageJsonPath, 'utf-8')) : {}
|
|
16
|
+
|
|
17
|
+
const name = packageJson.name ?? "Example"
|
|
18
|
+
const brandName = process.env.BRAND_NAME || (name[0].toUpperCase() + name.slice(1))
|
|
19
|
+
const homepage = process.env.BASE_HREF ?? packageJson.homepage
|
|
20
|
+
const brandDomain = process.env.BRAND_DOMAIN ||
|
|
21
|
+
(homepage && homepage.match(/https\:\/\/([^\/]+)/)?.[1]) || 'example.com'
|
|
22
|
+
const baseHref = process.env.BASE_HREF || homepage || 'http://localhost:8001'
|
|
23
|
+
const version = process.env.VERSION || packageJson.version
|
|
24
|
+
|
|
25
|
+
const clientConfig = {
|
|
26
|
+
version,
|
|
27
|
+
name, brandName, brandDomain, homepage, baseHref
|
|
28
|
+
}
|
|
29
|
+
const baseAppConfig = {
|
|
30
|
+
...clientConfig,
|
|
31
|
+
clientConfig: { ...clientConfig },
|
|
32
|
+
}
|
|
6
33
|
|
|
7
34
|
const contactTypes = ['email', 'phone']
|
|
8
35
|
const remoteAccountTypes = ['google', 'linkedin']
|
|
@@ -10,6 +37,10 @@ const remoteAccountTypes = ['google', 'linkedin']
|
|
|
10
37
|
import securityConfig from './security.config.js'
|
|
11
38
|
|
|
12
39
|
app.config = {
|
|
40
|
+
...baseAppConfig,
|
|
41
|
+
clientConfig: {
|
|
42
|
+
...baseAppConfig.clientConfig,
|
|
43
|
+
},
|
|
13
44
|
services: [
|
|
14
45
|
{
|
|
15
46
|
name: 'timer',
|
package/e2e/codecept.conf.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { devices } from 'playwright'
|
|
2
|
-
|
|
3
|
-
const testServerPort = process.env.TEST_URL ? 0 : require('get-port-sync')()
|
|
4
|
-
const testServerUrl = process.env.TEST_URL || `http://localhost:${testServerPort}`
|
|
5
|
-
|
|
6
|
-
const device = devices['Pixel 2']
|
|
7
|
-
|
|
8
|
-
exports.config = {
|
|
9
|
-
tests: './*.test.js',
|
|
10
|
-
output: './output',
|
|
11
|
-
helpers: {
|
|
12
|
-
LiveChange: {
|
|
13
|
-
require: '@live-change/codeceptjs-helper',
|
|
14
|
-
startServer: !process.env.TEST_URL,
|
|
15
|
-
enableSessions: true,
|
|
16
|
-
initScript: "./init.js",
|
|
17
|
-
port: testServerPort,
|
|
18
|
-
dev: true
|
|
19
|
-
},
|
|
20
|
-
VideoHelper: {
|
|
21
|
-
require: 'codeceptjs-video-helper'
|
|
22
|
-
},
|
|
23
|
-
AssertWrapper : {
|
|
24
|
-
require: "codeceptjs-assert"
|
|
25
|
-
},
|
|
26
|
-
Playwright: {
|
|
27
|
-
browser: 'chromium',
|
|
28
|
-
url: testServerUrl,
|
|
29
|
-
show: true,
|
|
30
|
-
emulate: {
|
|
31
|
-
...device,
|
|
32
|
-
recordVideo: process.env.RECORD_TESTS ? {
|
|
33
|
-
dir: "./output",
|
|
34
|
-
//size: { width: 1080, height: 1920 }
|
|
35
|
-
} : undefined,
|
|
36
|
-
},
|
|
37
|
-
chromium: {
|
|
38
|
-
args: [`--force-device-scale-factor=${device.deviceScaleFactor}`]
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
include: {
|
|
43
|
-
I: './steps_file.js'
|
|
44
|
-
},
|
|
45
|
-
bootstrap: null,
|
|
46
|
-
mocha: {},
|
|
47
|
-
name: 'e2e',
|
|
48
|
-
plugins: {
|
|
49
|
-
pauseOnFail: {},
|
|
50
|
-
retryFailedStep: {
|
|
51
|
-
enabled: true
|
|
52
|
-
},
|
|
53
|
-
tryTo: {
|
|
54
|
-
enabled: true
|
|
55
|
-
},
|
|
56
|
-
screenshotOnFail: {
|
|
57
|
-
enabled: true
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
const app = require('@live-change/framework').app()
|
|
2
|
-
const randomProfile = require('random-profile-generator')
|
|
3
|
-
|
|
4
|
-
const email = randomProfile.profile().firstName.toLowerCase() + '@test.com' // test domain - emails not sent
|
|
5
|
-
const email2 = randomProfile.profile().firstName.toLowerCase() + '2@test.com' // test domain - emails not sent
|
|
6
|
-
|
|
7
|
-
const happyPath = false
|
|
8
|
-
|
|
9
|
-
Feature('user')
|
|
10
|
-
|
|
11
|
-
Scenario('connect email with code', async ({ I }) => {
|
|
12
|
-
|
|
13
|
-
const user = app.generateUid()
|
|
14
|
-
|
|
15
|
-
const User = await I.haveModel('user', 'User')
|
|
16
|
-
const Email = await I.haveModel('email', 'Email')
|
|
17
|
-
const AuthenticatedUser = await I.haveModel('user', 'AuthenticatedUser')
|
|
18
|
-
|
|
19
|
-
await User.create({ id: user, roles: [] })
|
|
20
|
-
await Email.create({ id: email, email, user })
|
|
21
|
-
I.amOnPage('/')
|
|
22
|
-
const session = await I.executeScript(() => api.client.value.session)
|
|
23
|
-
await AuthenticatedUser.create({ id: session, user, session })
|
|
24
|
-
|
|
25
|
-
I.amOnPage('/connected')
|
|
26
|
-
I.see(email)
|
|
27
|
-
I.dontSee(email2)
|
|
28
|
-
|
|
29
|
-
I.click('button#connect')
|
|
30
|
-
|
|
31
|
-
I.seeInCurrentUrl('/connect')
|
|
32
|
-
|
|
33
|
-
if(!happyPath) {
|
|
34
|
-
I.fillField('input#email', email)
|
|
35
|
-
I.click('button[type=submit]')
|
|
36
|
-
I.seeInCurrentUrl('/connect')
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
I.fillField('input#email', email2)
|
|
40
|
-
I.click('button[type=submit]')
|
|
41
|
-
|
|
42
|
-
I.seeInCurrentUrl('/sent/')
|
|
43
|
-
|
|
44
|
-
const url = await I.grabCurrentUrl()
|
|
45
|
-
const authentication = url.split('/').pop()
|
|
46
|
-
|
|
47
|
-
const authenticationData = await I.grabObject('messageAuthentication', 'Authentication', authentication)
|
|
48
|
-
console.log("AUTHENTICATION DATA", authenticationData)
|
|
49
|
-
I.assert(!!authenticationData, true, 'authentication created')
|
|
50
|
-
I.assert(authenticationData?.messageData?.user, user, 'authentication contains user')
|
|
51
|
-
|
|
52
|
-
await I.useSecretCode(authentication, happyPath)
|
|
53
|
-
|
|
54
|
-
I.seeInCurrentUrl('/connect-finished')
|
|
55
|
-
|
|
56
|
-
if(!happyPath) {
|
|
57
|
-
I.amOnPage(url)
|
|
58
|
-
I.seeInCurrentUrl('/connect-finished')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
})
|