55f91d2cdf
* fix(workspace): auto approval * fix(scopes): access scopes across the server * fix(hasAccessRole): establish for all mutations * feat(token): scoping does not require the token to exist * chore(scopes): added additional roles * fix: replaced UNAUTHORIZED_ACCESS_ERROR with UNAUTHORIZED * fix(email): user list scopes
287 lines
9.2 KiB
TypeScript
287 lines
9.2 KiB
TypeScript
import { EmailVerifications, UserEmails, Users } from '@/modules/core/dbSchema'
|
|
import type { BasicTestUser } from '@/test/authHelper'
|
|
import { createTestUser, createTestUsers } from '@/test/authHelper'
|
|
import { buildApp, truncateTables } from '@/test/hooks'
|
|
import request from 'supertest'
|
|
import { expect } from 'chai'
|
|
import {
|
|
deleteOldAndInsertNewVerificationFactory,
|
|
deleteVerificationsFactory,
|
|
getPendingTokenFactory
|
|
} from '@/modules/emails/repositories'
|
|
import {
|
|
getPendingEmailVerificationStatus,
|
|
requestVerification
|
|
} from '@/test/graphql/users'
|
|
import { getEmailVerificationFinalizationRoute } from '@/modules/core/helpers/routeHelper'
|
|
import type { Express } from 'express'
|
|
import dayjs from 'dayjs'
|
|
import type { ServerAndContext } from '@/test/graphqlHelper'
|
|
import { createAuthedTestContext, createTestContext } from '@/test/graphqlHelper'
|
|
import { buildApolloServer } from '@/app'
|
|
import { db } from '@/db/knex'
|
|
import { requestEmailVerificationFactory } from '@/modules/emails/services/verification/request'
|
|
import { findPrimaryEmailForUserFactory } from '@/modules/core/repositories/userEmails'
|
|
import { sendEmail } from '@/modules/emails/services/sending'
|
|
import { renderEmail } from '@/modules/emails/services/emailRendering'
|
|
import { getUserFactory } from '@/modules/core/repositories/users'
|
|
import { getServerInfoFactory } from '@/modules/core/repositories/server'
|
|
import type { TestEmailListener } from '@/test/speckle-helpers/email'
|
|
import { createEmailListener } from '@/test/speckle-helpers/email'
|
|
|
|
const getUser = getUserFactory({ db })
|
|
const getPendingToken = getPendingTokenFactory({ db })
|
|
const deleteVerifications = deleteVerificationsFactory({ db })
|
|
const requestEmailVerification = requestEmailVerificationFactory({
|
|
getUser,
|
|
getServerInfo: getServerInfoFactory({ db }),
|
|
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({
|
|
db
|
|
}),
|
|
findPrimaryEmailForUser: findPrimaryEmailForUserFactory({ db }),
|
|
sendEmail,
|
|
renderEmail
|
|
})
|
|
|
|
const cleanup = async () => {
|
|
await truncateTables([Users.name, EmailVerifications.name, UserEmails.name])
|
|
}
|
|
|
|
describe('Email verifications @emails', () => {
|
|
const userA: BasicTestUser = {
|
|
name: 'd1',
|
|
email: 'd.1@speckle.systems',
|
|
id: ''
|
|
}
|
|
const userB: BasicTestUser = {
|
|
name: 'd2',
|
|
email: 'd.2@speckle.systems',
|
|
id: ''
|
|
}
|
|
|
|
let emailListener: TestEmailListener
|
|
|
|
before(async () => {
|
|
await cleanup()
|
|
await createTestUsers([userA, userB])
|
|
emailListener = await createEmailListener()
|
|
})
|
|
|
|
after(async () => {
|
|
await cleanup()
|
|
await emailListener.destroy()
|
|
})
|
|
|
|
afterEach(async () => {
|
|
emailListener.reset()
|
|
})
|
|
|
|
it('sends out 1 verification email immediately after new account creation', async () => {
|
|
const { getSends } = emailListener.listen({ times: 2 })
|
|
|
|
const newGuy: BasicTestUser = {
|
|
name: 'happy to be here',
|
|
email: 'happy@golucky.com',
|
|
password: 'googoogaga',
|
|
id: ''
|
|
}
|
|
|
|
await createTestUser(newGuy)
|
|
|
|
const sentEmails = getSends()
|
|
const emailParams = sentEmails[0]
|
|
expect(emailParams).to.be.ok
|
|
expect(emailParams.subject).to.contain('Speckle account email verification')
|
|
|
|
const verification = await getPendingToken({ email: newGuy.email })
|
|
expect(verification).to.be.ok
|
|
|
|
// There should be only 1 email!
|
|
expect(sentEmails.length).to.eq(1)
|
|
})
|
|
|
|
describe('when authenticated', () => {
|
|
let apollo: ServerAndContext
|
|
|
|
before(async () => {
|
|
apollo = {
|
|
apollo: await buildApolloServer(),
|
|
context: await createAuthedTestContext(userA.id)
|
|
}
|
|
})
|
|
|
|
it('pending verification is reported correctly', async () => {
|
|
await deleteVerifications(userA.email)
|
|
|
|
const preResult = await getPendingEmailVerificationStatus(apollo, {})
|
|
expect(preResult).to.not.haveGraphQLErrors()
|
|
expect(preResult.data?.user?.hasPendingVerification).to.be.false
|
|
|
|
await requestEmailVerification(userA.id)
|
|
|
|
const postResult = await getPendingEmailVerificationStatus(apollo, {})
|
|
expect(postResult).to.not.haveGraphQLErrors()
|
|
expect(postResult.data?.user?.hasPendingVerification).to.be.true
|
|
})
|
|
|
|
it("pending verificaton can't be retrieved for another user", async () => {
|
|
const preResult = await getPendingEmailVerificationStatus(apollo, {
|
|
id: userB.id
|
|
})
|
|
expect(preResult).to.haveGraphQLErrors()
|
|
expect(preResult.data?.user?.hasPendingVerification).to.be.null
|
|
})
|
|
|
|
describe('and requesting verification', () => {
|
|
const invokeRequestVerification = async (user: BasicTestUser) => {
|
|
const apollo = {
|
|
apollo: await buildApolloServer(),
|
|
context: await createAuthedTestContext(user.id)
|
|
}
|
|
return await requestVerification(apollo, {})
|
|
}
|
|
|
|
it('it succeeds', async () => {
|
|
// delete previous requests for userA, if any
|
|
await deleteVerifications(userA.email)
|
|
|
|
const { getSends } = emailListener.listen({ times: 2 })
|
|
|
|
const result = await invokeRequestVerification(userA)
|
|
expect(result).to.not.haveGraphQLErrors()
|
|
expect(result.data?.requestVerification).to.be.true
|
|
|
|
const sentEmails = getSends()
|
|
expect(sentEmails.length).to.eq(1)
|
|
|
|
const emailParams = sentEmails[0]
|
|
expect(emailParams).to.be.ok
|
|
expect(emailParams.subject).to.contain('Speckle account email verification')
|
|
expect(emailParams.html).to.be.ok
|
|
expect(emailParams.text).to.be.ok
|
|
|
|
const token = await getPendingToken({ email: userA.email })
|
|
expect(token).to.be.ok
|
|
})
|
|
|
|
it('it fails if user is already verified', async () => {
|
|
const verifiedUser: BasicTestUser = {
|
|
name: 'im verified hello',
|
|
email: 'verified@guy',
|
|
password: 'fasdasdasdasdasd',
|
|
verified: true,
|
|
id: ''
|
|
}
|
|
await createTestUser(verifiedUser)
|
|
|
|
const result = await invokeRequestVerification(verifiedUser)
|
|
|
|
expect(result.data?.requestVerification).to.be.not.ok
|
|
expect(result).to.haveGraphQLErrors('is already verified')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when not authenticated', () => {
|
|
let apollo: ServerAndContext
|
|
|
|
before(async () => {
|
|
apollo = {
|
|
apollo: await buildApolloServer(),
|
|
context: await createTestContext()
|
|
}
|
|
})
|
|
|
|
it('cant request an account verification', async () => {
|
|
const result = await requestVerification(apollo)
|
|
|
|
expect(result).to.haveGraphQLErrors('Must provide an auth token')
|
|
expect(result.data?.requestVerification).to.not.be.ok
|
|
})
|
|
|
|
describe('and finalizing verification', () => {
|
|
let app: Express
|
|
|
|
// prop values will be randomly generated in each test
|
|
let userWithPendingRequest: BasicTestUser = {
|
|
name: '',
|
|
email: '',
|
|
id: ''
|
|
}
|
|
|
|
let requestToken: string
|
|
|
|
before(async () => {
|
|
const { app: newApp } = await buildApp()
|
|
app = newApp
|
|
})
|
|
|
|
beforeEach(async () => {
|
|
// re-create for each test
|
|
const randomNumber = Math.ceil(Math.random() * 123456)
|
|
userWithPendingRequest = {
|
|
name: 'pending-req-guy' + randomNumber,
|
|
email: '',
|
|
id: ''
|
|
}
|
|
|
|
await createTestUser(userWithPendingRequest)
|
|
await requestEmailVerification(userWithPendingRequest.id)
|
|
requestToken =
|
|
(await getPendingToken({ email: userWithPendingRequest.email }))?.id || ''
|
|
})
|
|
|
|
it('it fails without a token', async () => {
|
|
const result = await request(app).get('/auth/verifyemail')
|
|
expect(result.statusCode).to.eq(302)
|
|
|
|
const location = decodeURIComponent(result.headers['location'] || '')
|
|
expect(location).to.contain('Missing verification token')
|
|
})
|
|
|
|
it('it fails with an invalid token', async () => {
|
|
const url = getEmailVerificationFinalizationRoute('aaabbbccdd')
|
|
|
|
const result = await request(app).get(url)
|
|
|
|
expect(result.statusCode).to.eq(302)
|
|
|
|
const location = decodeURIComponent(result.headers['location'] || '')
|
|
expect(location).to.contain('Invalid or expired verification token')
|
|
})
|
|
|
|
it('it fails with an expired token', async () => {
|
|
await EmailVerifications.knex()
|
|
.where(EmailVerifications.withoutTablePrefix.col.id, requestToken)
|
|
.update(
|
|
EmailVerifications.withoutTablePrefix.col.createdAt,
|
|
dayjs().subtract(8, 'day').toISOString()
|
|
)
|
|
|
|
const url = getEmailVerificationFinalizationRoute(requestToken)
|
|
|
|
const result = await request(app).get(url)
|
|
|
|
expect(result.statusCode).to.eq(302)
|
|
|
|
const location = decodeURIComponent(result.headers['location'] || '')
|
|
expect(location).to.contain('Invalid or expired verification token')
|
|
})
|
|
|
|
it('it succeeds with a valid token', async () => {
|
|
const url = getEmailVerificationFinalizationRoute(requestToken)
|
|
|
|
const result = await request(app).get(url)
|
|
|
|
expect(result.statusCode).to.eq(302)
|
|
|
|
const location = decodeURIComponent(result.headers['location'] || '')
|
|
expect(location).to.contain('emailverifiedstatus=true')
|
|
|
|
const userData = await getUser(userWithPendingRequest.id)
|
|
expect(userData?.verified).to.be.true
|
|
})
|
|
})
|
|
})
|
|
})
|