diff --git a/src/auth/auth.spec.ts b/src/auth/auth.spec.ts deleted file mode 100644 index cba1200cce..0000000000 --- a/src/auth/auth.spec.ts +++ /dev/null @@ -1,237 +0,0 @@ -import MongoClient from 'mongodb'; -import getConfig from '../config/load'; -import { email, password, connection } from '../mongoose/testCredentials'; - -require('isomorphic-fetch'); - -const { url: mongoURL, port: mongoPort, name: mongoDBName } = connection; - -const { serverURL: url } = getConfig(); - -let token = null; - -describe('Users REST API', () => { - it('should prevent registering a first user', async () => { - const response = await fetch(`${url}/api/admins/first-register`, { - body: JSON.stringify({ - email: 'thisuser@shouldbeprevented.com', - password: 'get-out', - }), - headers: { - 'Content-Type': 'application/json', - }, - method: 'post', - }); - - expect(response.status).toBe(403); - }); - - it('should login a user successfully', async () => { - const response = await fetch(`${url}/api/admins/login`, { - body: JSON.stringify({ - email, - password, - }), - headers: { - 'Content-Type': 'application/json', - }, - method: 'post', - }); - - const data = await response.json(); - - expect(response.status).toBe(200); - expect(data.token).toBeDefined(); - - ({ token } = data); - }); - - it('should return a logged in user from /me', async () => { - const response = await fetch(`${url}/api/admins/me`, { - headers: { - Authorization: `JWT ${token}`, - }, - }); - - const data = await response.json(); - - expect(response.status).toBe(200); - expect(data.user.email).toBeDefined(); - }); - - it('should refresh a token and reset its expiration', async () => { - const response = await fetch(`${url}/api/admins/refresh-token`, { - method: 'post', - headers: { - Authorization: `JWT ${token}`, - }, - }); - - const data = await response.json(); - - expect(response.status).toBe(200); - expect(data.refreshedToken).toBeDefined(); - - token = data.refreshedToken; - }); - - it('should allow forgot-password by email', async () => { - // TODO: figure out how to spy on payload instance functions - // const mailSpy = jest.spyOn(payload, 'sendEmail'); - const response = await fetch(`${url}/api/admins/forgot-password`, { - method: 'post', - body: JSON.stringify({ - email, - }), - headers: { - 'Content-Type': 'application/json', - }, - }); - - // is not working - // expect(mailSpy).toHaveBeenCalled(); - - expect(response.status).toBe(200); - }); - - it('should allow a user to be created', async () => { - const response = await fetch(`${url}/api/admins`, { - body: JSON.stringify({ - email: 'name@test.com', - password, - roles: ['editor'], - }), - headers: { - Authorization: `JWT ${token}`, - 'Content-Type': 'application/json', - }, - method: 'post', - }); - - const data = await response.json(); - - expect(response.status).toBe(201); - expect(data).toHaveProperty('message'); - expect(data).toHaveProperty('doc'); - - const { doc } = data; - - expect(doc).toHaveProperty('email'); - expect(doc).toHaveProperty('createdAt'); - expect(doc).toHaveProperty('roles'); - }); - - it('should allow verification of a user', async () => { - const emailToVerify = 'verify@me.com'; - const response = await fetch(`${url}/api/public-users`, { - body: JSON.stringify({ - email: emailToVerify, - password, - roles: ['editor'], - }), - headers: { - Authorization: `JWT ${token}`, - 'Content-Type': 'application/json', - }, - method: 'post', - }); - - expect(response.status).toBe(201); - const client = await MongoClient.connect(`${mongoURL}:${mongoPort}`, { - useUnifiedTopology: true, - }); - - const db = client.db(mongoDBName); - const userResult = await db.collection('public-users').findOne({ email: emailToVerify }); - const { _verified, _verificationToken } = userResult; - - expect(_verified).toBe(false); - expect(_verificationToken).toBeDefined(); - - const verificationResponse = await fetch(`${url}/api/public-users/verify/${_verificationToken}`, { - headers: { - 'Content-Type': 'application/json', - }, - method: 'post', - }); - - expect(verificationResponse.status).toBe(200); - - const afterVerifyResult = await db.collection('public-users').findOne({ email: emailToVerify }); - const { _verified: afterVerified, _verificationToken: afterToken } = afterVerifyResult; - expect(afterVerified).toBe(true); - expect(afterToken).toBeUndefined(); - }); - - describe('Account Locking', () => { - const userEmail = 'lock@me.com'; - let db; - beforeAll(async () => { - const client = await MongoClient.connect(`${mongoURL}:${mongoPort}`); - db = client.db(mongoDBName); - }); - - it('should lock the user after too many attempts', async () => { - await fetch(`${url}/api/admins`, { - body: JSON.stringify({ - email: userEmail, - password, - }), - headers: { - Authorization: `JWT ${token}`, - 'Content-Type': 'application/json', - }, - method: 'post', - }); - - const tryLogin = () => fetch(`${url}/api/admins/login`, { - body: JSON.stringify({ - email: userEmail, - password: 'bad', - }), - headers: { - Authorization: `JWT ${token}`, - 'Content-Type': 'application/json', - }, - method: 'post', - }); - - await tryLogin(); - await tryLogin(); - await tryLogin(); - await tryLogin(); - await tryLogin(); - - const userResult = await db.collection('admins').findOne({ email: userEmail }); - const { loginAttempts, lockUntil } = userResult; - - expect(loginAttempts).toBe(5); - expect(lockUntil).toBeDefined(); - }); - - it('should unlock account once lockUntil period is over', async () => { - await db.collection('admins').findOneAndUpdate( - { email: userEmail }, - { $set: { lockUntil: Date.now() - (605 * 1000) } }, - ); - - await fetch(`${url}/api/admins/login`, { - body: JSON.stringify({ - email: userEmail, - password, - }), - headers: { - Authorization: `JWT ${token}`, - 'Content-Type': 'application/json', - }, - method: 'post', - }); - - const userResult = await db.collection('admins').findOne({ email: userEmail }); - const { loginAttempts, lockUntil } = userResult; - - expect(loginAttempts).toBe(0); - expect(lockUntil).toBeUndefined(); - }); - }); -}); diff --git a/test/auth/config.ts b/test/auth/config.ts index 8cf17ae0f8..be7065b4e5 100644 --- a/test/auth/config.ts +++ b/test/auth/config.ts @@ -10,11 +10,31 @@ export default buildConfig({ { slug, auth: { - verify: true, - useAPIKey: true, + tokenExpiration: 7200, // 2 hours + verify: false, maxLoginAttempts: 2, + lockTime: 600 * 1000, // lock time in ms + useAPIKey: true, + depth: 0, + cookies: { + secure: false, + sameSite: 'lax', + domain: undefined, + }, }, - fields: [], + fields: [ + { + name: 'roles', + label: 'Role', + type: 'select', + options: ['admin', 'editor', 'moderator', 'user', 'viewer'], + defaultValue: 'user', + required: true, + saveToJWT: true, + hasMany: true, + }, + + ], }, ], }); diff --git a/test/auth/int.spec.ts b/test/auth/int.spec.ts new file mode 100644 index 0000000000..74fba81cc5 --- /dev/null +++ b/test/auth/int.spec.ts @@ -0,0 +1,290 @@ +import mongoose from 'mongoose'; +import payload from '../../src'; +import { initPayloadTest } from '../helpers/configHelpers'; +import { slug } from './config'; +import { devUser } from '../credentials'; + +require('isomorphic-fetch'); + +let apiUrl; + +const headers = { + 'Content-Type': 'application/json', +}; +const { email, password } = devUser; + +describe('Auth', () => { + beforeAll(async () => { + const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } }); + apiUrl = `${serverURL}/api`; + }); + + afterAll(async () => { + await mongoose.connection.dropDatabase(); + await mongoose.connection.close(); + await payload.mongoMemoryServer.stop(); + }); + + describe('admin user', () => { + beforeAll(async () => { + await fetch(`${apiUrl}/${slug}/first-register`, { + body: JSON.stringify({ + email, + password, + }), + headers, + method: 'post', + }); + }); + + it('should prevent registering a new first user', async () => { + const response = await fetch(`${apiUrl}/${slug}/first-register`, { + body: JSON.stringify({ + email: 'thisuser@shouldbeprevented.com', + password: 'get-out', + }), + headers, + method: 'post', + }); + + expect(response.status).toBe(403); + }); + + it('should login a user successfully', async () => { + const response = await fetch(`${apiUrl}/${slug}/login`, { + body: JSON.stringify({ + email, + password, + }), + headers, + method: 'post', + }); + + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.token).toBeDefined(); + }); + + describe('logged in', () => { + let token: string | undefined; + beforeAll(async () => { + const response = await fetch(`${apiUrl}/${slug}/login`, { + body: JSON.stringify({ + email, + password, + }), + headers, + method: 'post', + }); + + const data = await response.json(); + token = data.token; + }); + + it('should return a logged in user from /me', async () => { + const response = await fetch(`${apiUrl}/${slug}/me`, { + headers: { + ...headers, + Authorization: `JWT ${token}`, + }, + }); + + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.user.email).toBeDefined(); + }); + + it('should refresh a token and reset its expiration', async () => { + const response = await fetch(`${apiUrl}/${slug}/refresh-token`, { + method: 'post', + headers: { + Authorization: `JWT ${token}`, + }, + }); + + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.refreshedToken).toBeDefined(); + }); + + it('should allow a user to be created', async () => { + const response = await fetch(`${apiUrl}/${slug}`, { + body: JSON.stringify({ + email: 'name@test.com', + password, + roles: ['editor'], + }), + headers: { + Authorization: `JWT ${token}`, + 'Content-Type': 'application/json', + }, + method: 'post', + }); + + const data = await response.json(); + + expect(response.status).toBe(201); + expect(data).toHaveProperty('message'); + expect(data).toHaveProperty('doc'); + + const { doc } = data; + + expect(doc).toHaveProperty('email'); + expect(doc).toHaveProperty('createdAt'); + expect(doc).toHaveProperty('roles'); + }); + + it.skip('should allow verification of a user', async () => { + const emailToVerify = 'verify@me.com'; + const response = await fetch(`${apiUrl}/public-users`, { + body: JSON.stringify({ + email: emailToVerify, + password, + roles: ['editor'], + }), + headers: { + Authorization: `JWT ${token}`, + 'Content-Type': 'application/json', + }, + method: 'post', + }); + + expect(response.status).toBe(201); + // const client = await MongoClient.connect(`${mongoURL}:${mongoPort}`, { + // useUnifiedTopology: true, + // }); + + // const db = client.db(mongoDBName); + const { db } = mongoose.connection; + const userResult = await db.collection('public-users').findOne({ email: emailToVerify }); + // @ts-expect-error trust + const { _verified, _verificationToken } = userResult; + + expect(_verified).toBe(false); + expect(_verificationToken).toBeDefined(); + + const verificationResponse = await fetch(`${apiUrl}/public-users/verify/${_verificationToken}`, { + headers: { + 'Content-Type': 'application/json', + }, + method: 'post', + }); + + expect(verificationResponse.status).toBe(200); + + const afterVerifyResult = await db.collection('public-users').findOne({ email: emailToVerify }); + const { _verified: afterVerified, _verificationToken: afterToken } = afterVerifyResult; + expect(afterVerified).toBe(true); + expect(afterToken).toBeUndefined(); + }); + + describe('Account Locking', () => { + const userEmail = 'lock@me.com'; + + const tryLogin = async () => { + await fetch(`${apiUrl}/${slug}/login`, { + body: JSON.stringify({ + email: userEmail, + password: 'bad', + }), + headers: { + 'Content-Type': 'application/json', + }, + method: 'post', + }); + // expect(loginRes.status).toEqual(401); + }; + + beforeAll(async () => { + const response = await fetch(`${apiUrl}/${slug}/login`, { + body: JSON.stringify({ + email, + password, + }), + headers, + method: 'post', + }); + + const data = await response.json(); + token = data.token; + + // New user to lock + await fetch(`${apiUrl}/${slug}`, { + body: JSON.stringify({ + email: userEmail, + password, + }), + headers: { + 'Content-Type': 'application/json', + Authorization: `JWT ${token}`, + }, + method: 'post', + }); + }); + + it('should lock the user after too many attempts', async () => { + await tryLogin(); + await tryLogin(); + + const userResult = await mongoose.connection.db.collection(slug).findOne({ email: userEmail }); + const { loginAttempts, lockUntil } = userResult; + + expect(loginAttempts).toBe(2); + expect(lockUntil).toBeDefined(); + }); + + it('should unlock account once lockUntil period is over', async () => { + // Lock user + await tryLogin(); + await tryLogin(); + + // set lockUntil + await mongoose.connection.db + .collection(slug) + .findOneAndUpdate({ email: userEmail }, { $set: { lockUntil: Date.now() - 605 * 1000 } }); + + // login + await fetch(`${apiUrl}/${slug}/login`, { + body: JSON.stringify({ + email: userEmail, + password, + }), + headers: { + Authorization: `JWT ${token}`, + 'Content-Type': 'application/json', + }, + method: 'post', + }); + + const userResult = await mongoose.connection.db + .collection(slug) + .findOne({ email: userEmail }); + const { loginAttempts, lockUntil } = userResult; + + expect(loginAttempts).toBe(0); + expect(lockUntil).toBeUndefined(); + }); + }); + }); + + it('should allow forgot-password by email', async () => { + // TODO: Spy on payload sendEmail function + const response = await fetch(`${apiUrl}/${slug}/forgot-password`, { + method: 'post', + body: JSON.stringify({ + email, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + + // expect(mailSpy).toHaveBeenCalled(); + + expect(response.status).toBe(200); + }); + }); +}); diff --git a/test/auth/payload-types.ts b/test/auth/payload-types.ts index 0315d69a4d..fe69f84827 100644 --- a/test/auth/payload-types.ts +++ b/test/auth/payload-types.ts @@ -18,10 +18,9 @@ export interface User { email?: string; resetPasswordToken?: string; resetPasswordExpiration?: string; - _verified?: boolean; - _verificationToken?: string; loginAttempts?: number; lockUntil?: string; + roles: ('admin' | 'editor' | 'moderator' | 'user' | 'viewer')[]; createdAt: string; updatedAt: string; }