omnivore/packages/api/test/routers/auth.test.ts

635 lines
18 KiB
TypeScript
Raw Permalink Normal View History

2024-04-05 14:53:03 +00:00
import { expect } from 'chai'
2024-08-22 11:06:50 +00:00
import sinon, { SinonFakeTimers } from 'sinon'
2022-07-21 11:19:38 +00:00
import supertest from 'supertest'
2023-08-23 04:41:02 +00:00
import { StatusType, User } from '../../src/entity/user'
2023-08-23 04:15:50 +00:00
import { getRepository } from '../../src/repository'
2023-09-06 14:53:58 +00:00
import { userRepository } from '../../src/repository/user'
2024-04-02 02:21:57 +00:00
import { isValidSignupRequest } from '../../src/routers/auth/auth_router'
2023-03-14 06:52:42 +00:00
import { AuthProvider } from '../../src/routers/auth/auth_types'
import { createPendingUserToken } from '../../src/routers/auth/jwt_helpers'
import { searchAndCountLibraryItems } from '../../src/services/library_item'
2023-09-07 04:35:05 +00:00
import { deleteUser, updateUser } from '../../src/services/user'
2022-07-22 09:41:43 +00:00
import {
comparePassword,
generateVerificationToken,
hashPassword,
} from '../../src/utils/auth'
2023-09-07 04:35:05 +00:00
import { createTestUser } from '../db'
2023-03-14 06:52:42 +00:00
import { generateFakeUuid, request } from '../util'
2022-07-26 14:08:10 +00:00
2022-07-21 11:19:38 +00:00
describe('auth router', () => {
const route = '/api/auth'
describe('email signup', () => {
const signupRequest = (
email: string,
password: string,
name: string,
username: string
): supertest.Test => {
return request.post(`${route}/email-signup`).send({
email,
password,
name,
username,
})
}
2022-07-21 11:19:38 +00:00
const validPassword = 'validPassword'
let email: string
let password: string
let username: string
let name: string
context('when inputs are valid and user not exists', () => {
before(() => {
password = validPassword
username = 'Some_username'
2025-09-24 10:37:39 +00:00
email = `${username}@omnivore.work ` // space at the end is intentional
2022-07-21 11:19:38 +00:00
name = 'Some name'
})
afterEach(async () => {
2023-09-07 08:43:24 +00:00
const user = await userRepository.findOneBy({ name })
2023-09-07 04:35:05 +00:00
await deleteUser(user!.id)
2022-07-21 11:19:38 +00:00
})
context('when confirmation email sent', () => {
it('redirects to verify email', async () => {
2022-07-21 11:19:38 +00:00
const res = await signupRequest(
email,
password,
name,
username
).expect(302)
expect(res.header.location).to.endWith(
'/verify-email?message=SIGNUP_SUCCESS'
2022-07-21 11:19:38 +00:00
)
})
it('creates the user with pending status and correct name', async () => {
await signupRequest(email, password, name, username).expect(302)
2023-09-07 08:43:24 +00:00
const user = await userRepository.findOneBy({ name })
2022-07-21 11:19:38 +00:00
expect(user?.status).to.eql(StatusType.Pending)
expect(user?.name).to.eql(name)
})
})
})
context('when user exists', () => {
2022-08-10 08:36:31 +00:00
let user: User
2022-07-21 11:19:38 +00:00
before(async () => {
username = 'Some_username'
2022-08-10 08:36:31 +00:00
user = await createTestUser(username)
2022-07-21 11:19:38 +00:00
email = user.email
password = 'Some password'
})
after(async () => {
2023-09-07 04:35:05 +00:00
await deleteUser(user.id)
2022-07-21 11:19:38 +00:00
})
2024-04-19 03:19:39 +00:00
it('redirects to sign up page with error code UNKNOWN', async () => {
2022-07-21 11:19:38 +00:00
const res = await signupRequest(email, password, name, username).expect(
302
)
expect(res.header.location).to.endWith(
2024-04-19 03:19:39 +00:00
'/email-signup?errorCodes=UNKNOWN'
2022-07-21 11:19:38 +00:00
)
})
})
context('when username is invalid', () => {
before(() => {
email = 'Some_email'
password = validPassword
username = 'omnivore_admin'
})
it('redirects to sign up page with error code INVALID_USERNAME', async () => {
const res = await signupRequest(email, password, name, username).expect(
302
)
expect(res.header.location).to.endWith(
2022-07-21 11:22:33 +00:00
'/email-signup?errorCodes=INVALID_USERNAME'
2022-07-21 11:19:38 +00:00
)
})
})
2023-05-10 15:09:04 +00:00
context('when password is over max length', () => {
before(() => {
email = 'Some_email'
password = 'badpass'.repeat(100)
username = 'omnivore_admin'
})
2023-05-11 01:09:34 +00:00
it('redirects to sign up page with error code INVALID_CREDENTIALS', async () => {
2023-05-10 15:09:04 +00:00
const res = await signupRequest(email, password, name, username).expect(
302
)
expect(res.header.location).to.endWith(
2023-05-11 01:09:34 +00:00
'/email-signup?errorCodes=INVALID_CREDENTIALS'
2023-05-10 15:09:04 +00:00
)
})
})
2022-07-21 11:19:38 +00:00
})
2022-07-21 12:17:14 +00:00
describe('login', () => {
const loginRequest = (email: string, password: string): supertest.Test => {
return request.post(`${route}/email-login`).send({
email,
password,
})
}
const correctPassword = 'correctPassword'
let user: User
let email: string
let password: string
before(async () => {
const hashedPassword = await hashPassword(correctPassword)
user = await createTestUser('login_test_user', undefined, hashedPassword)
})
after(async () => {
2023-09-07 04:35:05 +00:00
await deleteUser(user.id)
2022-07-21 12:17:14 +00:00
})
context('when email and password are valid', () => {
before(() => {
2023-03-14 06:52:42 +00:00
email = user.email + ' ' // space at the end is intentional
2022-07-21 12:17:14 +00:00
password = correctPassword
})
it('redirects to sso page', async () => {
2022-07-21 12:17:14 +00:00
const res = await loginRequest(email, password).expect(302)
expect(res.header.location).to.contain('/api/client/auth?tok')
2022-07-21 12:17:14 +00:00
})
2022-07-21 13:01:00 +00:00
it('set auth token in cookie', async () => {
const res = await loginRequest(email, password).expect(302)
expect(res.header['set-cookie']).to.be.an('array')
expect(res.header['set-cookie'][0]).to.contain('auth')
})
2022-07-21 12:17:14 +00:00
})
2024-04-05 09:13:10 +00:00
context('when user is not confirmed', () => {
2022-07-22 11:44:00 +00:00
beforeEach(async () => {
2023-09-07 04:35:05 +00:00
await updateUser(user.id, { status: StatusType.Pending })
2022-07-22 11:20:14 +00:00
email = user.email
password = correctPassword
})
2022-07-22 11:44:00 +00:00
afterEach(async () => {
2023-09-07 04:35:05 +00:00
await updateUser(user.id, { status: StatusType.Active })
})
it('redirects with error code PendingVerification', async () => {
const res = await loginRequest(email, password).expect(302)
expect(res.header.location).to.endWith(
'/email-login?errorCodes=PENDING_VERIFICATION'
)
})
})
2022-07-21 12:17:14 +00:00
context('when user not exists', () => {
before(() => {
email = 'Some email'
})
it('redirects with error code UserNotFound', async () => {
const res = await loginRequest(email, password).expect(302)
expect(res.header.location).to.endWith(
'/email-login?errorCodes=USER_NOT_FOUND'
)
})
})
2024-04-05 09:13:10 +00:00
context('when user has no password stored in db', () => {
before(async () => {
2023-09-07 04:35:05 +00:00
await updateUser(user.id, { password: '' })
2022-07-22 11:20:14 +00:00
email = user.email
password = user.password!
2022-07-21 12:17:14 +00:00
})
after(async () => {
2023-09-07 04:35:05 +00:00
await updateUser(user.id, { password })
2022-07-21 12:17:14 +00:00
})
it('redirects with error code WrongSource', async () => {
const res = await loginRequest(email, password).expect(302)
expect(res.header.location).to.endWith(
'/email-login?errorCodes=WRONG_SOURCE'
)
})
})
context('when password is wrong', () => {
before(() => {
email = user.email
password = 'Wrong password'
})
it('redirects with error code InvalidCredentials', async () => {
const res = await loginRequest(email, password).expect(302)
expect(res.header.location).to.endWith(
'/email-login?errorCodes=INVALID_CREDENTIALS'
)
})
})
})
2022-07-21 15:01:35 +00:00
describe('confirm-email', () => {
const confirmEmailRequest = (token: string): supertest.Test => {
2022-07-21 16:00:02 +00:00
return request.post(`${route}/confirm-email`).send({ token })
2022-07-21 15:01:35 +00:00
}
let user: User
let token: string
before(async () => {
user = await createTestUser('pendingUser', undefined, 'password', true)
})
after(async () => {
2023-09-07 04:35:05 +00:00
await deleteUser(user.id)
2022-07-21 15:01:35 +00:00
})
context('when token is valid', () => {
2024-08-22 11:06:50 +00:00
beforeEach(async () => {
token = await generateVerificationToken({ id: user.id })
2022-07-21 15:01:35 +00:00
})
2022-07-22 09:41:43 +00:00
it('set auth token in cookie', async () => {
2022-07-21 15:01:35 +00:00
const res = await confirmEmailRequest(token).expect(302)
2022-07-22 09:41:43 +00:00
expect(res.header['set-cookie']).to.be.an('array')
expect(res.header['set-cookie'][0]).to.contain('auth')
})
2022-07-26 22:58:38 +00:00
it('redirects to sso page', async () => {
2022-07-22 09:41:43 +00:00
const res = await confirmEmailRequest(token).expect(302)
2022-07-26 22:58:38 +00:00
expect(res.header.location).to.contain('/api/client/auth?tok')
2022-07-21 15:01:35 +00:00
})
it('sets user as active', async () => {
await confirmEmailRequest(token).expect(302)
const updatedUser = await getRepository(User).findOneBy({
name: user.name,
})
expect(updatedUser?.status).to.eql(StatusType.Active)
})
})
context('when token is invalid', () => {
it('redirects to confirm-email with error code InvalidToken', async () => {
const res = await confirmEmailRequest('invalid_token').expect(302)
expect(res.header.location).to.endWith(
'/confirm-email?errorCodes=INVALID_TOKEN'
)
})
})
context('when token is expired', () => {
2024-08-22 11:06:50 +00:00
let clock: SinonFakeTimers
before(async () => {
2024-08-22 11:06:50 +00:00
clock = sinon.useFakeTimers()
token = await generateVerificationToken({ id: user.id })
// advance time by 1 hour
clock.tick(60 * 60 * 1000)
})
after(() => {
clock.restore()
2022-07-21 15:01:35 +00:00
})
it('redirects to confirm-email page with error code TokenExpired', async () => {
const res = await confirmEmailRequest(token).expect(302)
expect(res.header.location).to.endWith(
'/confirm-email?errorCodes=TOKEN_EXPIRED'
)
})
})
context('when user is not found', () => {
before(async () => {
2022-07-21 15:01:35 +00:00
const nonExistsUserId = generateFakeUuid()
token = await generateVerificationToken({ id: nonExistsUserId })
2022-07-21 15:01:35 +00:00
})
it('redirects to confirm-email page with error code UserNotFound', async () => {
const res = await confirmEmailRequest(token).expect(302)
expect(res.header.location).to.endWith(
'/confirm-email?errorCodes=USER_NOT_FOUND'
)
})
})
})
2022-07-22 08:38:21 +00:00
describe('forgot-password', () => {
const emailResetPasswordReq = (email: string): supertest.Test => {
2022-07-22 08:38:21 +00:00
return request.post(`${route}/forgot-password`).send({
email,
})
}
let email: string
context('when email is not empty', () => {
before(() => {
email = `some_email@domain.app`
})
context('when user exists', () => {
let user: User
before(async () => {
user = await createTestUser('test_user')
email = user.email
})
after(async () => {
2023-09-07 04:35:05 +00:00
await deleteUser(user.id)
})
context('when email is verified', () => {
before(async () => {
2023-09-07 04:35:05 +00:00
await updateUser(user.id, { status: StatusType.Active })
})
context('when reset password email sent', () => {
2022-07-22 08:38:21 +00:00
it('redirects to forgot-password page with success message', async () => {
const res = await emailResetPasswordReq(email).expect(302)
expect(res.header.location).to.endWith('/auth/reset-sent')
})
})
})
context('when email is not verified', () => {
before(async () => {
2023-09-07 04:35:05 +00:00
await updateUser(user.id, { status: StatusType.Pending })
})
it('redirects to email-login page with error code PENDING_VERIFICATION', async () => {
const res = await emailResetPasswordReq(email).expect(302)
expect(res.header.location).to.endWith('/auth/reset-sent')
})
})
})
context('when user does not exist', () => {
before(() => {
email = 'non_exists_email@domain.app'
})
2022-07-22 08:38:21 +00:00
it('redirects to forgot-password page with error code USER_NOT_FOUND', async () => {
const res = await emailResetPasswordReq(email).expect(302)
expect(res.header.location).to.endWith('/auth/reset-sent')
})
})
})
2024-04-05 10:02:52 +00:00
context('when email is empty', () => {
before(() => {
email = ''
})
it('redirects to forgot-password page with error code INVALID_EMAIL', async () => {
const res = await emailResetPasswordReq(email).expect(302)
expect(res.header.location).to.endWith(
'/forgot-password?errorCodes=INVALID_EMAIL'
)
})
})
})
2022-07-22 09:41:43 +00:00
describe('reset-password', () => {
const resetPasswordRequest = (
token: string,
password: string
): supertest.Test => {
return request.post(`${route}/reset-password`).send({
token,
password,
})
}
let user: User
let token: string
before(async () => {
user = await createTestUser('test_user', undefined, 'test_password')
})
after(async () => {
2023-09-07 04:35:05 +00:00
await deleteUser(user.id)
2022-07-22 09:41:43 +00:00
})
context('when token is valid', () => {
2024-08-22 11:06:50 +00:00
beforeEach(async () => {
token = await generateVerificationToken({ id: user.id })
2022-07-22 09:41:43 +00:00
})
context('when password is not empty', () => {
it('redirects to reset-password page with success message', async () => {
const res = await resetPasswordRequest(token, 'new_password').expect(
302
)
expect(res.header.location).to.contain('/api/client/auth?tok')
2022-07-22 09:41:43 +00:00
})
it('resets password', async () => {
const password = 'test_reset_password'
await resetPasswordRequest(token, password).expect(302)
const updatedUser = await getRepository(User).findOneBy({
id: user?.id,
})
2024-04-05 09:13:10 +00:00
const newPassword = updatedUser?.password || ''
expect(await comparePassword(password, newPassword)).to.be.true
2022-07-22 09:41:43 +00:00
})
})
context('when password is empty', () => {
it('redirects to reset-password page with error code INVALID_PASSWORD', async () => {
const res = await resetPasswordRequest(token, '').expect(302)
2022-07-26 22:58:38 +00:00
expect(res.header.location).to.match(
/.*\/auth\/reset-password\/(.*)?\?errorCodes=INVALID_PASSWORD/g
2022-07-22 09:41:43 +00:00
)
})
})
})
context('when token is invalid', () => {
it('redirects to reset-password page with error code InvalidToken', async () => {
const res = await resetPasswordRequest(
'invalid_token',
'new_password'
).expect(302)
2022-07-26 22:58:38 +00:00
expect(res.header.location).to.match(
/.*\/auth\/reset-password\/(.*)?\?errorCodes=INVALID_TOKEN/g
2022-07-22 09:41:43 +00:00
)
})
context('when token is expired', () => {
2024-08-22 11:06:50 +00:00
let clock: SinonFakeTimers
before(async () => {
2024-08-22 11:06:50 +00:00
clock = sinon.useFakeTimers()
token = await generateVerificationToken({ id: user.id })
// advance time by 1 hour
clock.tick(60 * 60 * 1000)
})
after(() => {
clock.restore()
2022-07-22 09:41:43 +00:00
})
it('redirects to reset-password page with error code ExpiredToken', async () => {
const res = await resetPasswordRequest(token, 'new_password').expect(
302
)
expect(res.header.location).to.endWith(
2022-07-26 22:58:38 +00:00
'/auth/reset-password/?errorCodes=TOKEN_EXPIRED'
2022-07-22 09:41:43 +00:00
)
})
})
})
})
2022-10-11 07:44:53 +00:00
describe('create account', () => {
const createAccountRequest = (
bio: string,
name: string,
username: string,
2022-10-12 06:30:39 +00:00
pendingUserAuth: string,
client: string
2022-10-11 07:44:53 +00:00
): supertest.Test => {
return request
.post(`${route}/create-account`)
2022-10-12 06:30:39 +00:00
.set('X-OmnivoreClient', client)
.set('User-Agent', 'chrome')
2022-10-11 07:44:53 +00:00
.set('Cookie', [`pendingUserAuth=${pendingUserAuth}`])
.send({
name,
bio,
username,
})
}
context('when inputs are valid and user not exists', () => {
2024-04-05 09:13:10 +00:00
const name = 'test_user'
const username = 'test_user'
const sourceUserId = 'test_source_user_id'
2025-09-24 10:37:39 +00:00
const email = 'test_user@omnivore.work'
2024-04-05 09:13:10 +00:00
const bio = 'test_bio'
const provider: AuthProvider = 'EMAIL'
2022-10-11 07:44:53 +00:00
2022-10-11 08:07:49 +00:00
afterEach(async () => {
2023-09-06 14:53:58 +00:00
const user = await userRepository.findOneByOrFail({ name })
2023-09-07 04:35:05 +00:00
await deleteUser(user.id)
2022-10-11 07:44:53 +00:00
})
it('adds popular reads to the continue reading section', async () => {
2022-10-11 07:44:53 +00:00
const pendingUserToken = await createPendingUserToken({
2022-10-11 08:07:49 +00:00
sourceUserId,
email,
2022-10-12 06:30:39 +00:00
provider,
2022-10-11 08:07:49 +00:00
name,
username,
})
await createAccountRequest(
bio,
name,
username,
2022-10-12 06:30:39 +00:00
pendingUserToken!,
'web'
2022-10-11 08:07:49 +00:00
).expect(200)
2023-09-11 14:58:57 +00:00
const user = await userRepository.findOneByOrFail({ name })
const { count } = await searchAndCountLibraryItems(
{ query: 'in:inbox sort:read-desc is:reading' },
2023-11-10 09:34:46 +00:00
user.id
)
2022-10-11 08:07:49 +00:00
expect(count).to.eql(3)
})
it('adds iOS popular reads to the library if provider is iOS', async () => {
const pendingUserToken = await createPendingUserToken({
sourceUserId,
email,
2022-10-12 06:30:39 +00:00
provider,
2022-10-11 07:44:53 +00:00
name,
username,
})
await createAccountRequest(
2022-10-11 08:07:49 +00:00
bio,
2022-10-11 07:44:53 +00:00
name,
username,
2022-10-12 06:30:39 +00:00
pendingUserToken!,
'ios'
2022-10-11 07:44:53 +00:00
).expect(200)
2023-09-06 14:53:58 +00:00
const user = await userRepository.findOneByOrFail({ name })
const { count } = await searchAndCountLibraryItems(
{ query: 'in:all' },
user.id
)
2022-10-11 07:44:53 +00:00
expect(count).to.eql(4)
2022-10-11 07:44:53 +00:00
})
})
})
2022-07-21 11:19:38 +00:00
})
2024-04-01 03:05:50 +00:00
describe('isValidSignupRequest', () => {
2024-04-05 09:13:10 +00:00
it('returns true for normal looking requests', () => {
2024-04-01 03:05:50 +00:00
const result = isValidSignupRequest({
2025-09-24 10:37:39 +00:00
email: 'email@omnivore.work',
2024-04-01 03:05:50 +00:00
password: 'superDuperPassword',
name: "The User's Name",
username: 'foouser',
})
expect(result).to.be.true
})
2024-04-05 09:13:10 +00:00
it('returns false for requests w/missing info', () => {
2024-04-01 03:05:50 +00:00
let result = isValidSignupRequest({
password: 'superDuperPassword',
name: "The User's Name",
username: 'foouser',
})
expect(result).to.be.false
result = isValidSignupRequest({
2025-09-24 10:37:39 +00:00
email: 'email@omnivore.work',
2024-04-01 03:05:50 +00:00
name: "The User's Name",
username: 'foouser',
})
expect(result).to.be.false
result = isValidSignupRequest({
2025-09-24 10:37:39 +00:00
email: 'email@omnivore.work',
2024-04-01 03:05:50 +00:00
password: 'superDuperPassword',
username: 'foouser',
})
expect(result).to.be.false
result = isValidSignupRequest({
2025-09-24 10:37:39 +00:00
email: 'email@omnivore.work',
2024-04-01 03:05:50 +00:00
password: 'superDuperPassword',
name: "The User's Name",
})
expect(result).to.be.false
})
2024-04-05 09:13:10 +00:00
it('returns false for requests w/malicious info', () => {
const result = isValidSignupRequest({
2024-04-01 03:05:50 +00:00
password: 'superDuperPassword',
name: "You've won a cake sign up here: https://foo.bar",
username: 'foouser',
})
expect(result).to.be.false
})
})