Files
speckle-server/packages/server/modules/emails/tests/verifications.spec.ts
T
Kristaps Fabians Geikins bde148f286 chore(server): migrating fully to ESM (#5042)
* wip

* some extra fixes

* stuff kinda works?

* need to figure out mocks

* need to figure out mocks

* fix db listener

* gqlgen fix

* minor gqlgen watch adjustment

* lint fixes

* delete old codegen file

* converting migrations to ESM

* getModuleDIrectory

* vitest sort of works

* added back ts-vitest

* resolve gql double load

* fixing test timeout configs

* TSC lint fix

* fix automate tests

* moar debugging

* debugging

* more debugging

* codegen update

* server works

* yargs migrated

* chore(server): getting rid of global mocks for Server ESM (#5046)

* got rid of email mock

* got rid of comment mocks

* got rid of multi region mocks

* got rid of stripe mock

* admin override mock updated

* removed final mock

* fixing import.meta.resolve calls

* another import.meta.resolve fix

* added requested test

* nyc ESM fix

* removed unneeded deps + linting

* yarn lock forgot to commit

* tryna fix flakyness

* email capture util fix

* sendEmail fix

* fix TSX check

* sender transporter fix + CR comments

* merge main fix

* test fixx

* circleci fix

* gqlgen bigint fix

* error formatter fix

* more error formatting improvements

* esmloader added to Dockerfile

* more dockerfile fixes

* bg jobs fix
2025-07-14 10:26:19 +03:00

288 lines
9.1 KiB
TypeScript

import { EmailVerifications, UserEmails, Users } from '@/modules/core/dbSchema'
import { BasicTestUser, 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 { Express } from 'express'
import dayjs from 'dayjs'
import {
createAuthedTestContext,
createTestContext,
ServerAndContext
} 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 { createEmailListener, TestEmailListener } 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
})
})
})
})