UserHelper.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. const { expect } = require('chai')
  2. const AuthenticationManager = require('../../../../app/src/Features/Authentication/AuthenticationManager')
  3. const Settings = require('@overleaf/settings')
  4. const InstitutionsAPI = require('../../../../app/src/Features/Institutions/InstitutionsAPI')
  5. const UserCreator = require('../../../../app/src/Features/User/UserCreator')
  6. const UserGetter = require('../../../../app/src/Features/User/UserGetter')
  7. const UserUpdater = require('../../../../app/src/Features/User/UserUpdater')
  8. const moment = require('moment')
  9. const request = require('request-promise-native')
  10. const { db } = require('../../../../app/src/infrastructure/mongodb')
  11. const { ObjectId } = require('mongodb')
  12. let globalUserNum = Settings.test.counterInit
  13. class UserHelper {
  14. /**
  15. * Create UserHelper
  16. * @param {object} [user] - Mongo User object
  17. */
  18. constructor(user = null) {
  19. // used for constructing default emails, etc
  20. this.userNum = globalUserNum++
  21. // initialize all internal state properties to defaults
  22. this.reset()
  23. // set user if passed in, may be null
  24. this.user = user
  25. }
  26. /* sync functions */
  27. /**
  28. * Get auditLog, ignore the login
  29. * @return {object[]}
  30. */
  31. getAuditLogWithoutNoise() {
  32. return (this.user.auditLog || []).filter(entry => {
  33. return entry.operation !== 'login'
  34. })
  35. }
  36. /**
  37. * Generate default email from unique (per instantiation) user number
  38. * @returns {string} email
  39. */
  40. getDefaultEmail() {
  41. return `test.user.${this.userNum}@example.com`
  42. }
  43. /**
  44. * Generate email, password args object. Default values will be used if
  45. * email and password are not passed in args.
  46. * @param {object} [userData]
  47. * @param {string} [userData.email] email to use
  48. * @param {string} [userData.password] password to use
  49. * @returns {object} email, password object
  50. */
  51. getDefaultEmailPassword(userData = {}) {
  52. return {
  53. email: this.getDefaultEmail(),
  54. password: this.getDefaultPassword(),
  55. ...userData,
  56. }
  57. }
  58. /**
  59. * Generate default password from unique (per instantiation) user number
  60. * @returns {string} password
  61. */
  62. getDefaultPassword() {
  63. return `New-Password-${this.userNum}!`
  64. }
  65. /**
  66. * (Re)set internal state of UserHelper object.
  67. */
  68. reset() {
  69. // cached csrf token
  70. this._csrfToken = ''
  71. // used to store mongo user object once created/loaded
  72. this.user = null
  73. // cookie jar
  74. this.jar = request.jar()
  75. // create new request instance
  76. this.request = request.defaults({})
  77. // initialize request instance with default options
  78. this.setRequestDefaults({
  79. baseUrl: UserHelper.baseUrl(),
  80. followRedirect: false,
  81. jar: this.jar,
  82. resolveWithFullResponse: true,
  83. })
  84. }
  85. /* Set defaults for request object. Applied over existing defaults.
  86. * @param {object} [defaults]
  87. */
  88. setRequestDefaults(defaults = {}) {
  89. // request-promise instance for making requests
  90. this.request = this.request.defaults(defaults)
  91. }
  92. /**
  93. * Make a request for the user and run expectations on the response.
  94. * @param {object} [requestOptions] options to pass to request
  95. * @param {object} [responseExpectations] expectations:
  96. * - {Int} statusCode the expected status code
  97. * - {RegEx} message a matcher for the message
  98. */
  99. async expectErrorOnRequest(requestOptions, responseExpectations) {
  100. let error
  101. try {
  102. await this.request(requestOptions)
  103. } catch (e) {
  104. error = e
  105. }
  106. expect(error).to.exist
  107. expect(error.statusCode).to.equal(responseExpectations.statusCode)
  108. expect(error.message).to.match(responseExpectations.message)
  109. }
  110. /* async http api call methods */
  111. /**
  112. * Requests csrf token unless already cached in internal state
  113. */
  114. async getCsrfToken() {
  115. // get csrf token from api and store
  116. const response = await this.request.get('/dev/csrf')
  117. this._csrfToken = response.body
  118. // use csrf token for requests
  119. this.setRequestDefaults({
  120. headers: { 'x-csrf-token': this._csrfToken },
  121. })
  122. }
  123. /**
  124. * Make request to POST /logout
  125. * @param {object} [options] options to pass to request
  126. * @returns {object} http response
  127. */
  128. async logout(options = {}) {
  129. // do not throw exception on 302
  130. options.simple = false
  131. // post logout
  132. const response = await this.request.post('/logout', options)
  133. if (
  134. response.statusCode !== 302 ||
  135. !response.headers.location.includes('/login')
  136. ) {
  137. throw new Error('logout failed')
  138. }
  139. // after logout CSRF token becomes invalid
  140. this._csrfToken = ''
  141. // resolve with http request response
  142. return response
  143. }
  144. /* static sync methods */
  145. /**
  146. * Generates base URL from env options
  147. * @returns {string} baseUrl
  148. */
  149. static baseUrl() {
  150. return `http://${process.env.HTTP_TEST_HOST || 'localhost'}:23000`
  151. }
  152. /* static async instantiation methods */
  153. /**
  154. * Create a new user via UserCreator and return UserHelper instance
  155. * @param {object} attributes user data for UserCreator
  156. * @param {object} options options for UserCreator
  157. * @returns {UserHelper}
  158. */
  159. static async createUser(attributes = {}) {
  160. const userHelper = new UserHelper()
  161. attributes = userHelper.getDefaultEmailPassword(attributes)
  162. // hash password and delete plaintext if set
  163. if (attributes.password) {
  164. attributes.hashedPassword =
  165. await AuthenticationManager.promises.hashPassword(attributes.password)
  166. delete attributes.password
  167. }
  168. userHelper.user = await UserCreator.promises.createNewUser(attributes)
  169. return userHelper
  170. }
  171. /**
  172. * Get existing user via UserGetter and return UserHelper instance.
  173. * All args passed to UserGetter.getUser.
  174. * @returns {UserHelper}
  175. */
  176. static async getUser(...args) {
  177. const user = await UserGetter.promises.getUser(...args)
  178. if (!user) {
  179. throw new Error(`no user found for args: ${JSON.stringify([...args])}`)
  180. }
  181. return new UserHelper(user)
  182. }
  183. /**
  184. * Update an existing user via UserUpdater and return the updated UserHelper
  185. * instance.
  186. * All args passed to UserUpdater.getUser.
  187. * @returns {UserHelper}
  188. */
  189. static async updateUser(userId, update) {
  190. // TODO(das7pad): revert back to args pass-through after mongo upgrades
  191. const user = await UserUpdater.promises.updateUser(
  192. { _id: ObjectId(userId) },
  193. update
  194. )
  195. if (!user) {
  196. throw new Error(`no user found for args: ${JSON.stringify([userId])}`)
  197. }
  198. return new UserHelper(user)
  199. }
  200. /**
  201. * Login to existing account via request and return UserHelper instance
  202. * @param {object} userData
  203. * @param {string} userData.email
  204. * @param {string} userData.password
  205. * @returns {UserHelper}
  206. */
  207. static async loginUser(userData) {
  208. if (!userData || !userData.email || !userData.password) {
  209. throw new Error('email and password required')
  210. }
  211. const userHelper = new UserHelper()
  212. const loginPath = Settings.enableLegacyLogin ? '/login/legacy' : '/login'
  213. await userHelper.getCsrfToken()
  214. const response = await userHelper.request.post(loginPath, {
  215. json: {
  216. 'g-recaptcha-response': 'valid',
  217. ...userData,
  218. },
  219. })
  220. if (response.statusCode !== 200 || response.body.redir !== '/project') {
  221. const error = new Error('login failed')
  222. error.response = response
  223. throw error
  224. }
  225. userHelper.user = await UserGetter.promises.getUser({
  226. email: userData.email,
  227. })
  228. if (!userHelper.user) {
  229. throw new Error(`user not found for email: ${userData.email}`)
  230. }
  231. await userHelper.getCsrfToken()
  232. return userHelper
  233. }
  234. /**
  235. * Check if user is logged in by requesting an endpoint behind authentication.
  236. * @returns {Boolean}
  237. */
  238. async isLoggedIn() {
  239. const response = await this.request.get('/user/sessions', {
  240. followRedirect: true,
  241. })
  242. return response.request.path === '/user/sessions'
  243. }
  244. /**
  245. * Register new account via request and return UserHelper instance.
  246. * If userData is not provided the default email and password will be used.
  247. * @param {object} [userData]
  248. * @param {string} [userData.email]
  249. * @param {string} [userData.password]
  250. * @returns {UserHelper}
  251. */
  252. static async registerUser(userData, options = {}) {
  253. const userHelper = new UserHelper()
  254. await userHelper.getCsrfToken()
  255. userData = userHelper.getDefaultEmailPassword(userData)
  256. options.json = userData
  257. const { body } = await userHelper.request.post('/register', options)
  258. if (body.message && body.message.type === 'error') {
  259. throw new Error(`register api error: ${body.message.text}`)
  260. }
  261. if (body.redir === '/institutional-login') {
  262. throw new Error(
  263. `cannot register intitutional email: ${options.json.email}`
  264. )
  265. }
  266. userHelper.user = await UserGetter.promises.getUser({
  267. email: userData.email,
  268. })
  269. if (!userHelper.user) {
  270. throw new Error(`user not found for email: ${userData.email}`)
  271. }
  272. await userHelper.getCsrfToken()
  273. return userHelper
  274. }
  275. async refreshMongoUser() {
  276. this.user = await UserGetter.promises.getUser({
  277. _id: this.user._id,
  278. })
  279. return this.user
  280. }
  281. async addEmail(email) {
  282. const response = await this.request.post({
  283. form: {
  284. email,
  285. },
  286. simple: false,
  287. uri: '/user/emails',
  288. })
  289. expect(response.statusCode).to.equal(204)
  290. }
  291. async addEmailAndConfirm(userId, email) {
  292. await this.addEmail(email)
  293. await this.confirmEmail(userId, email)
  294. }
  295. async changeConfirmationDate(userId, email, date) {
  296. const query = {
  297. _id: userId,
  298. 'emails.email': email,
  299. }
  300. const update = {
  301. $set: {
  302. 'emails.$.confirmedAt': date,
  303. 'emails.$.reconfirmedAt': date,
  304. },
  305. }
  306. await UserUpdater.promises.updateUser(query, update)
  307. await InstitutionsAPI.promises.addAffiliation(userId, email, {
  308. confirmedAt: date,
  309. })
  310. }
  311. async changeConfirmedToNotificationPeriod(
  312. userId,
  313. email,
  314. maxConfirmationMonths
  315. ) {
  316. // set a user's confirmation date so that
  317. // it is within the notification period to reconfirm
  318. // but not older than the last day to reconfirm
  319. const notificationDays = Settings.reconfirmNotificationDays
  320. if (!notificationDays) return
  321. const middleOfNotificationPeriod = Math.ceil(notificationDays / 2)
  322. // use the middle of the notification rather than the start or end due to
  323. // variations in days in months.
  324. const lastDayToReconfirm = moment().subtract(
  325. maxConfirmationMonths,
  326. 'months'
  327. )
  328. const notificationsStart = lastDayToReconfirm
  329. .add(middleOfNotificationPeriod, 'days')
  330. .toDate()
  331. await this.changeConfirmationDate(userId, email, notificationsStart)
  332. }
  333. async changeConfirmedToPastReconfirmation(
  334. userId,
  335. email,
  336. maxConfirmationMonths
  337. ) {
  338. // set a user's confirmation date so that they are past the reconfirmation window
  339. const date = moment()
  340. .subtract(maxConfirmationMonths, 'months')
  341. .subtract(1, 'week')
  342. .toDate()
  343. await this.changeConfirmationDate(userId, email, date)
  344. }
  345. async confirmEmail(userId, email) {
  346. let response
  347. // UserHelper.createUser does not create a confirmation token
  348. response = await this.request.post({
  349. form: {
  350. email,
  351. },
  352. simple: false,
  353. uri: '/user/emails/resend_confirmation',
  354. })
  355. expect(response.statusCode).to.equal(200)
  356. const tokenData = await db.tokens
  357. .find({
  358. use: 'email_confirmation',
  359. 'data.user_id': userId.toString(),
  360. 'data.email': email,
  361. usedAt: { $exists: false },
  362. })
  363. .next()
  364. response = await this.request.post({
  365. form: {
  366. token: tokenData.token,
  367. },
  368. simple: false,
  369. uri: '/user/emails/confirm',
  370. })
  371. expect(response.statusCode).to.equal(200)
  372. }
  373. }
  374. module.exports = UserHelper