atproto/packages/pds/tests/email-confirmation.test.ts
Eric Bailey 922b94ce37
Update email templates (#2767)
* Update email templates

* Update PLC

* Update test with new email string

* Format

* One more test update

* Use handle instead of identifier to match entryway

* Changeset
2024-09-12 10:53:31 -05:00

228 lines
6.8 KiB
TypeScript

import { once, EventEmitter } from 'events'
import Mail from 'nodemailer/lib/mailer'
import { AtpAgent } from '@atproto/api'
import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env'
import userSeed from './seeds/users'
import { ServerMailer } from '../src/mailer'
import {
ComAtprotoServerConfirmEmail,
ComAtprotoServerUpdateEmail,
} from '@atproto/api'
describe('email confirmation', () => {
let network: TestNetworkNoAppView
let agent: AtpAgent
let sc: SeedClient
let mailer: ServerMailer
const mailCatcher = new EventEmitter()
let _origSendMail
let alice
beforeAll(async () => {
network = await TestNetworkNoAppView.create({
dbPostgresSchema: 'email_confirmation',
})
// @ts-expect-error Error due to circular dependency with the dev-env package
mailer = network.pds.ctx.mailer
agent = network.pds.getClient()
sc = network.getSeedClient()
await userSeed(sc)
alice = sc.accounts[sc.dids.alice]
// Catch emails for use in tests
_origSendMail = mailer.transporter.sendMail
mailer.transporter.sendMail = async (opts) => {
const result = await _origSendMail.call(mailer.transporter, opts)
mailCatcher.emit('mail', opts)
return result
}
})
afterAll(async () => {
mailer.transporter.sendMail = _origSendMail
await network.close()
})
const getMailFrom = async (promise): Promise<Mail.Options> => {
const result = await Promise.all([once(mailCatcher, 'mail'), promise])
return result[0][0]
}
const getTokenFromMail = (mail: Mail.Options) =>
mail.html?.toString().match(/>([a-z0-9]{5}-[a-z0-9]{5})</i)?.[1]
it('starts a user out unverified', async () => {
const session = await agent.api.com.atproto.server.getSession(
{},
{ headers: sc.getHeaders(alice.did) },
)
expect(session.data.emailConfirmed).toEqual(false)
})
it('allows email update without token when unverified', async () => {
const res = await agent.api.com.atproto.server.requestEmailUpdate(
undefined,
{ headers: sc.getHeaders(alice.did) },
)
expect(res.data.tokenRequired).toBe(false)
await agent.api.com.atproto.server.updateEmail(
{
email: 'new-alice@example.com',
},
{ headers: sc.getHeaders(alice.did), encoding: 'application/json' },
)
const session = await agent.api.com.atproto.server.getSession(
{},
{ headers: sc.getHeaders(alice.did) },
)
expect(session.data.email).toEqual('new-alice@example.com')
expect(session.data.emailConfirmed).toEqual(false)
alice.email = session.data.email
})
let confirmToken
it('requests email confirmation', async () => {
const mail = await getMailFrom(
agent.api.com.atproto.server.requestEmailConfirmation(undefined, {
headers: sc.getHeaders(alice.did),
}),
)
expect(mail.to).toEqual(alice.email)
expect(mail.html).toContain('Confirm your email')
confirmToken = getTokenFromMail(mail)
expect(confirmToken).toBeDefined()
})
it('fails email confirmation with a bad token', async () => {
const attempt = agent.api.com.atproto.server.confirmEmail(
{
email: alice.email,
token: '123456',
},
{ headers: sc.getHeaders(alice.did), encoding: 'application/json' },
)
await expect(attempt).rejects.toThrow(
ComAtprotoServerConfirmEmail.InvalidTokenError,
)
})
it('fails email confirmation with a bad token', async () => {
const attempt = agent.api.com.atproto.server.confirmEmail(
{
email: 'fake-alice@example.com',
token: confirmToken,
},
{ headers: sc.getHeaders(alice.did), encoding: 'application/json' },
)
await expect(attempt).rejects.toThrow(
ComAtprotoServerConfirmEmail.InvalidEmailError,
)
})
it('confirms email', async () => {
await agent.api.com.atproto.server.confirmEmail(
{
email: alice.email,
token: confirmToken,
},
{ headers: sc.getHeaders(alice.did), encoding: 'application/json' },
)
const session = await agent.api.com.atproto.server.getSession(
{},
{ headers: sc.getHeaders(alice.did) },
)
expect(session.data.emailConfirmed).toBe(true)
})
it('disallows email update without token when verified', async () => {
const attempt = agent.api.com.atproto.server.updateEmail(
{
email: 'new-alice-2@example.com',
},
{ headers: sc.getHeaders(alice.did), encoding: 'application/json' },
)
await expect(attempt).rejects.toThrow(
ComAtprotoServerUpdateEmail.TokenRequiredError,
)
})
let updateToken
it('requests email update', async () => {
const reqUpdate = async () => {
const res = await agent.api.com.atproto.server.requestEmailUpdate(
undefined,
{
headers: sc.getHeaders(alice.did),
},
)
expect(res.data.tokenRequired).toBe(true)
}
const mail = await getMailFrom(reqUpdate())
expect(mail.to).toEqual(alice.email)
expect(mail.html).toContain('Update your email')
updateToken = getTokenFromMail(mail)
expect(updateToken).toBeDefined()
})
it('fails email update with a bad token', async () => {
const attempt = agent.api.com.atproto.server.updateEmail(
{
email: 'new-alice-2@example.com',
token: '123456',
},
{ headers: sc.getHeaders(alice.did), encoding: 'application/json' },
)
await expect(attempt).rejects.toThrow(
ComAtprotoServerUpdateEmail.InvalidTokenError,
)
})
it('fails email update with a badly formatted email', async () => {
const attempt = agent.api.com.atproto.server.updateEmail(
{
email: 'bad-email@disposeamail.com',
token: updateToken,
},
{ headers: sc.getHeaders(alice.did), encoding: 'application/json' },
)
await expect(attempt).rejects.toThrow(
'This email address is not supported, please use a different email.',
)
})
it('fails email update with in-use email', async () => {
const attempt = agent.api.com.atproto.server.updateEmail(
{
email: 'bob@test.com',
token: updateToken,
},
{ headers: sc.getHeaders(alice.did), encoding: 'application/json' },
)
await expect(attempt).rejects.toThrow(
'This email address is already in use, please use a different email.',
)
})
it('updates email', async () => {
await agent.api.com.atproto.server.updateEmail(
{
email: 'new-alice-2@example.com',
token: updateToken,
},
{ headers: sc.getHeaders(alice.did), encoding: 'application/json' },
)
const session = await agent.api.com.atproto.server.getSession(
{},
{ headers: sc.getHeaders(alice.did) },
)
expect(session.data.email).toBe('new-alice-2@example.com')
expect(session.data.emailConfirmed).toBe(false)
})
})