| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416 |
- const { expect } = require('chai')
- const AuthenticationManager = require('../../../../app/src/Features/Authentication/AuthenticationManager')
- const Settings = require('@overleaf/settings')
- const InstitutionsAPI = require('../../../../app/src/Features/Institutions/InstitutionsAPI')
- const UserCreator = require('../../../../app/src/Features/User/UserCreator')
- const UserGetter = require('../../../../app/src/Features/User/UserGetter')
- const UserUpdater = require('../../../../app/src/Features/User/UserUpdater')
- const moment = require('moment')
- const request = require('request-promise-native')
- const { db } = require('../../../../app/src/infrastructure/mongodb')
- const { ObjectId } = require('mongodb')
- let globalUserNum = Settings.test.counterInit
- class UserHelper {
- /**
- * Create UserHelper
- * @param {object} [user] - Mongo User object
- */
- constructor(user = null) {
- // used for constructing default emails, etc
- this.userNum = globalUserNum++
- // initialize all internal state properties to defaults
- this.reset()
- // set user if passed in, may be null
- this.user = user
- }
- /* sync functions */
- /**
- * Get auditLog, ignore the login
- * @return {object[]}
- */
- getAuditLogWithoutNoise() {
- return (this.user.auditLog || []).filter(entry => {
- return entry.operation !== 'login'
- })
- }
- /**
- * Generate default email from unique (per instantiation) user number
- * @returns {string} email
- */
- getDefaultEmail() {
- return `test.user.${this.userNum}@example.com`
- }
- /**
- * Generate email, password args object. Default values will be used if
- * email and password are not passed in args.
- * @param {object} [userData]
- * @param {string} [userData.email] email to use
- * @param {string} [userData.password] password to use
- * @returns {object} email, password object
- */
- getDefaultEmailPassword(userData = {}) {
- return {
- email: this.getDefaultEmail(),
- password: this.getDefaultPassword(),
- ...userData,
- }
- }
- /**
- * Generate default password from unique (per instantiation) user number
- * @returns {string} password
- */
- getDefaultPassword() {
- return `New-Password-${this.userNum}!`
- }
- /**
- * (Re)set internal state of UserHelper object.
- */
- reset() {
- // cached csrf token
- this._csrfToken = ''
- // used to store mongo user object once created/loaded
- this.user = null
- // cookie jar
- this.jar = request.jar()
- // create new request instance
- this.request = request.defaults({})
- // initialize request instance with default options
- this.setRequestDefaults({
- baseUrl: UserHelper.baseUrl(),
- followRedirect: false,
- jar: this.jar,
- resolveWithFullResponse: true,
- })
- }
- /* Set defaults for request object. Applied over existing defaults.
- * @param {object} [defaults]
- */
- setRequestDefaults(defaults = {}) {
- // request-promise instance for making requests
- this.request = this.request.defaults(defaults)
- }
- /**
- * Make a request for the user and run expectations on the response.
- * @param {object} [requestOptions] options to pass to request
- * @param {object} [responseExpectations] expectations:
- * - {Int} statusCode the expected status code
- * - {RegEx} message a matcher for the message
- */
- async expectErrorOnRequest(requestOptions, responseExpectations) {
- let error
- try {
- await this.request(requestOptions)
- } catch (e) {
- error = e
- }
- expect(error).to.exist
- expect(error.statusCode).to.equal(responseExpectations.statusCode)
- expect(error.message).to.match(responseExpectations.message)
- }
- /* async http api call methods */
- /**
- * Requests csrf token unless already cached in internal state
- */
- async getCsrfToken() {
- // get csrf token from api and store
- const response = await this.request.get('/dev/csrf')
- this._csrfToken = response.body
- // use csrf token for requests
- this.setRequestDefaults({
- headers: { 'x-csrf-token': this._csrfToken },
- })
- }
- /**
- * Make request to POST /logout
- * @param {object} [options] options to pass to request
- * @returns {object} http response
- */
- async logout(options = {}) {
- // do not throw exception on 302
- options.simple = false
- // post logout
- const response = await this.request.post('/logout', options)
- if (
- response.statusCode !== 302 ||
- !response.headers.location.includes('/login')
- ) {
- throw new Error('logout failed')
- }
- // after logout CSRF token becomes invalid
- this._csrfToken = ''
- // resolve with http request response
- return response
- }
- /* static sync methods */
- /**
- * Generates base URL from env options
- * @returns {string} baseUrl
- */
- static baseUrl() {
- return `http://${process.env.HTTP_TEST_HOST || 'localhost'}:23000`
- }
- /* static async instantiation methods */
- /**
- * Create a new user via UserCreator and return UserHelper instance
- * @param {object} attributes user data for UserCreator
- * @param {object} options options for UserCreator
- * @returns {UserHelper}
- */
- static async createUser(attributes = {}) {
- const userHelper = new UserHelper()
- attributes = userHelper.getDefaultEmailPassword(attributes)
- // hash password and delete plaintext if set
- if (attributes.password) {
- attributes.hashedPassword =
- await AuthenticationManager.promises.hashPassword(attributes.password)
- delete attributes.password
- }
- userHelper.user = await UserCreator.promises.createNewUser(attributes)
- return userHelper
- }
- /**
- * Get existing user via UserGetter and return UserHelper instance.
- * All args passed to UserGetter.getUser.
- * @returns {UserHelper}
- */
- static async getUser(...args) {
- const user = await UserGetter.promises.getUser(...args)
- if (!user) {
- throw new Error(`no user found for args: ${JSON.stringify([...args])}`)
- }
- return new UserHelper(user)
- }
- /**
- * Update an existing user via UserUpdater and return the updated UserHelper
- * instance.
- * All args passed to UserUpdater.getUser.
- * @returns {UserHelper}
- */
- static async updateUser(userId, update) {
- // TODO(das7pad): revert back to args pass-through after mongo upgrades
- const user = await UserUpdater.promises.updateUser(
- { _id: ObjectId(userId) },
- update
- )
- if (!user) {
- throw new Error(`no user found for args: ${JSON.stringify([userId])}`)
- }
- return new UserHelper(user)
- }
- /**
- * Login to existing account via request and return UserHelper instance
- * @param {object} userData
- * @param {string} userData.email
- * @param {string} userData.password
- * @returns {UserHelper}
- */
- static async loginUser(userData) {
- if (!userData || !userData.email || !userData.password) {
- throw new Error('email and password required')
- }
- const userHelper = new UserHelper()
- const loginPath = Settings.enableLegacyLogin ? '/login/legacy' : '/login'
- await userHelper.getCsrfToken()
- const response = await userHelper.request.post(loginPath, {
- json: {
- 'g-recaptcha-response': 'valid',
- ...userData,
- },
- })
- if (response.statusCode !== 200 || response.body.redir !== '/project') {
- const error = new Error('login failed')
- error.response = response
- throw error
- }
- userHelper.user = await UserGetter.promises.getUser({
- email: userData.email,
- })
- if (!userHelper.user) {
- throw new Error(`user not found for email: ${userData.email}`)
- }
- await userHelper.getCsrfToken()
- return userHelper
- }
- /**
- * Check if user is logged in by requesting an endpoint behind authentication.
- * @returns {Boolean}
- */
- async isLoggedIn() {
- const response = await this.request.get('/user/sessions', {
- followRedirect: true,
- })
- return response.request.path === '/user/sessions'
- }
- /**
- * Register new account via request and return UserHelper instance.
- * If userData is not provided the default email and password will be used.
- * @param {object} [userData]
- * @param {string} [userData.email]
- * @param {string} [userData.password]
- * @returns {UserHelper}
- */
- static async registerUser(userData, options = {}) {
- const userHelper = new UserHelper()
- await userHelper.getCsrfToken()
- userData = userHelper.getDefaultEmailPassword(userData)
- options.json = userData
- const { body } = await userHelper.request.post('/register', options)
- if (body.message && body.message.type === 'error') {
- throw new Error(`register api error: ${body.message.text}`)
- }
- if (body.redir === '/institutional-login') {
- throw new Error(
- `cannot register intitutional email: ${options.json.email}`
- )
- }
- userHelper.user = await UserGetter.promises.getUser({
- email: userData.email,
- })
- if (!userHelper.user) {
- throw new Error(`user not found for email: ${userData.email}`)
- }
- await userHelper.getCsrfToken()
- return userHelper
- }
- async refreshMongoUser() {
- this.user = await UserGetter.promises.getUser({
- _id: this.user._id,
- })
- return this.user
- }
- async addEmail(email) {
- const response = await this.request.post({
- form: {
- email,
- },
- simple: false,
- uri: '/user/emails',
- })
- expect(response.statusCode).to.equal(204)
- }
- async addEmailAndConfirm(userId, email) {
- await this.addEmail(email)
- await this.confirmEmail(userId, email)
- }
- async changeConfirmationDate(userId, email, date) {
- const query = {
- _id: userId,
- 'emails.email': email,
- }
- const update = {
- $set: {
- 'emails.$.confirmedAt': date,
- 'emails.$.reconfirmedAt': date,
- },
- }
- await UserUpdater.promises.updateUser(query, update)
- await InstitutionsAPI.promises.addAffiliation(userId, email, {
- confirmedAt: date,
- })
- }
- async changeConfirmedToNotificationPeriod(
- userId,
- email,
- maxConfirmationMonths
- ) {
- // set a user's confirmation date so that
- // it is within the notification period to reconfirm
- // but not older than the last day to reconfirm
- const notificationDays = Settings.reconfirmNotificationDays
- if (!notificationDays) return
- const middleOfNotificationPeriod = Math.ceil(notificationDays / 2)
- // use the middle of the notification rather than the start or end due to
- // variations in days in months.
- const lastDayToReconfirm = moment().subtract(
- maxConfirmationMonths,
- 'months'
- )
- const notificationsStart = lastDayToReconfirm
- .add(middleOfNotificationPeriod, 'days')
- .toDate()
- await this.changeConfirmationDate(userId, email, notificationsStart)
- }
- async changeConfirmedToPastReconfirmation(
- userId,
- email,
- maxConfirmationMonths
- ) {
- // set a user's confirmation date so that they are past the reconfirmation window
- const date = moment()
- .subtract(maxConfirmationMonths, 'months')
- .subtract(1, 'week')
- .toDate()
- await this.changeConfirmationDate(userId, email, date)
- }
- async confirmEmail(userId, email) {
- let response
- // UserHelper.createUser does not create a confirmation token
- response = await this.request.post({
- form: {
- email,
- },
- simple: false,
- uri: '/user/emails/resend_confirmation',
- })
- expect(response.statusCode).to.equal(200)
- const tokenData = await db.tokens
- .find({
- use: 'email_confirmation',
- 'data.user_id': userId.toString(),
- 'data.email': email,
- usedAt: { $exists: false },
- })
- .next()
- response = await this.request.post({
- form: {
- token: tokenData.token,
- },
- simple: false,
- uri: '/user/emails/confirm',
- })
- expect(response.statusCode).to.equal(200)
- }
- }
- module.exports = UserHelper
|