UserHelper.js 10.0 KB

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