UserMembershipControllerTests.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. import sinon from 'sinon'
  2. import { expect } from 'chai'
  3. import esmock from 'esmock'
  4. import MockRequest from '../helpers/MockRequest.js'
  5. import MockResponse from '../helpers/MockResponse.js'
  6. import EntityConfigs from '../../../../app/src/Features/UserMembership/UserMembershipEntityConfigs.js'
  7. import Errors from '../../../../app/src/Features/Errors/Errors.js'
  8. import {
  9. UserIsManagerError,
  10. UserNotFoundError,
  11. UserAlreadyAddedError,
  12. } from '../../../../app/src/Features/UserMembership/UserMembershipErrors.js'
  13. const assertCalledWith = sinon.assert.calledWith
  14. const modulePath =
  15. '../../../../app/src/Features/UserMembership/UserMembershipController.mjs'
  16. describe('UserMembershipController', function () {
  17. beforeEach(async function () {
  18. this.req = new MockRequest()
  19. this.req.params.id = 'mock-entity-id'
  20. this.user = { _id: 'mock-user-id' }
  21. this.newUser = { _id: 'mock-new-user-id', email: 'new-user-email@foo.bar' }
  22. this.subscription = {
  23. _id: 'mock-subscription-id',
  24. fetchV1Data: callback => callback(null, this.subscription),
  25. }
  26. this.institution = {
  27. _id: 'mock-institution-id',
  28. v1Id: 123,
  29. fetchV1Data: callback => {
  30. const institution = Object.assign({}, this.institution)
  31. institution.name = 'Test Institution Name'
  32. callback(null, institution)
  33. },
  34. }
  35. this.users = [
  36. {
  37. _id: 'mock-member-id-1',
  38. email: 'mock-email-1@foo.com',
  39. last_logged_in_at: '2020-08-09T12:43:11.467Z',
  40. last_active_at: '2021-08-09T12:43:11.467Z',
  41. },
  42. {
  43. _id: 'mock-member-id-2',
  44. email: 'mock-email-2@foo.com',
  45. last_logged_in_at: '2020-05-20T10:41:11.407Z',
  46. last_active_at: '2021-05-20T10:41:11.407Z',
  47. },
  48. ]
  49. this.Settings = {
  50. managedUsers: {
  51. enabled: false,
  52. },
  53. }
  54. this.SessionManager = {
  55. getSessionUser: sinon.stub().returns(this.user),
  56. getLoggedInUserId: sinon.stub().returns(this.user._id),
  57. }
  58. this.SSOConfig = {
  59. findById: sinon
  60. .stub()
  61. .returns({ exec: sinon.stub().resolves({ enabled: true }) }),
  62. }
  63. this.UserMembershipHandler = {
  64. getEntity: sinon.stub().yields(null, this.subscription),
  65. createEntity: sinon.stub().yields(null, this.institution),
  66. getUsers: sinon.stub().yields(null, this.users),
  67. addUser: sinon.stub().yields(null, this.newUser),
  68. removeUser: sinon.stub().yields(null),
  69. promises: {
  70. getUsers: sinon.stub().resolves(this.users),
  71. },
  72. }
  73. this.SplitTestHandler = {
  74. promises: {
  75. getAssignment: sinon.stub().resolves({ variant: 'default' }),
  76. },
  77. getAssignment: sinon.stub().yields(null, { variant: 'default' }),
  78. }
  79. this.UserMembershipController = await esmock.strict(modulePath, {
  80. '../../../../app/src/Features/UserMembership/UserMembershipErrors': {
  81. UserIsManagerError,
  82. UserNotFoundError,
  83. UserAlreadyAddedError,
  84. },
  85. '../../../../app/src/Features/Authentication/SessionManager':
  86. this.SessionManager,
  87. '../../../../app/src/Features/SplitTests/SplitTestHandler':
  88. this.SplitTestHandler,
  89. '../../../../app/src/Features/UserMembership/UserMembershipHandler':
  90. this.UserMembershipHandler,
  91. '@overleaf/settings': this.Settings,
  92. '../../../../app/src/models/SSOConfig': { SSOConfig: this.SSOConfig },
  93. })
  94. })
  95. describe('index', function () {
  96. beforeEach(function () {
  97. this.req.entity = this.subscription
  98. this.req.entityConfig = EntityConfigs.group
  99. })
  100. it('get users', async function () {
  101. await this.UserMembershipController.manageGroupMembers(this.req, {
  102. render: () => {
  103. sinon.assert.calledWithMatch(
  104. this.UserMembershipHandler.promises.getUsers,
  105. this.subscription,
  106. { modelName: 'Subscription' }
  107. )
  108. },
  109. })
  110. })
  111. it('render group view', async function () {
  112. this.subscription.managedUsersEnabled = false
  113. await this.UserMembershipController.manageGroupMembers(this.req, {
  114. render: (viewPath, viewParams) => {
  115. expect(viewPath).to.equal('user_membership/group-members-react')
  116. expect(viewParams.users).to.deep.equal(this.users)
  117. expect(viewParams.groupSize).to.equal(this.subscription.membersLimit)
  118. expect(viewParams.managedUsersActive).to.equal(false)
  119. },
  120. })
  121. })
  122. it('render group view with managed users', async function () {
  123. this.subscription.managedUsersEnabled = true
  124. await this.UserMembershipController.manageGroupMembers(this.req, {
  125. render: (viewPath, viewParams) => {
  126. expect(viewPath).to.equal('user_membership/group-members-react')
  127. expect(viewParams.users).to.deep.equal(this.users)
  128. expect(viewParams.groupSize).to.equal(this.subscription.membersLimit)
  129. expect(viewParams.managedUsersActive).to.equal(true)
  130. },
  131. })
  132. })
  133. it('render group managers view', async function () {
  134. this.req.entityConfig = EntityConfigs.groupManagers
  135. await this.UserMembershipController.manageGroupManagers(this.req, {
  136. render: (viewPath, viewParams) => {
  137. expect(viewPath).to.equal('user_membership/group-managers-react')
  138. expect(viewParams.groupSize).to.equal(undefined)
  139. },
  140. })
  141. })
  142. it('render institution view', async function () {
  143. this.req.entity = this.institution
  144. this.req.entityConfig = EntityConfigs.institution
  145. await this.UserMembershipController.manageInstitutionManagers(this.req, {
  146. render: (viewPath, viewParams) => {
  147. expect(viewPath).to.equal(
  148. 'user_membership/institution-managers-react'
  149. )
  150. expect(viewParams.name).to.equal('Test Institution Name')
  151. expect(viewParams.groupSize).to.equal(undefined)
  152. },
  153. })
  154. })
  155. })
  156. describe('add', function () {
  157. beforeEach(function () {
  158. this.req.body.email = this.newUser.email
  159. this.req.entity = this.subscription
  160. this.req.entityConfig = EntityConfigs.groupManagers
  161. })
  162. it('add user', function (done) {
  163. this.UserMembershipController.add(this.req, {
  164. json: () => {
  165. sinon.assert.calledWithMatch(
  166. this.UserMembershipHandler.addUser,
  167. this.subscription,
  168. { modelName: 'Subscription' },
  169. this.newUser.email
  170. )
  171. done()
  172. },
  173. })
  174. })
  175. it('return user object', function (done) {
  176. this.UserMembershipController.add(this.req, {
  177. json: payload => {
  178. payload.user.should.equal(this.newUser)
  179. done()
  180. },
  181. })
  182. })
  183. it('handle readOnly entity', function (done) {
  184. this.req.entityConfig = EntityConfigs.group
  185. this.UserMembershipController.add(this.req, null, error => {
  186. expect(error).to.exist
  187. expect(error).to.be.an.instanceof(Errors.NotFoundError)
  188. done()
  189. })
  190. })
  191. it('handle user already added', function (done) {
  192. this.UserMembershipHandler.addUser.yields(new UserAlreadyAddedError())
  193. this.UserMembershipController.add(this.req, {
  194. status: () => ({
  195. json: payload => {
  196. expect(payload.error.code).to.equal('user_already_added')
  197. done()
  198. },
  199. }),
  200. })
  201. })
  202. it('handle user not found', function (done) {
  203. this.UserMembershipHandler.addUser.yields(new UserNotFoundError())
  204. this.UserMembershipController.add(this.req, {
  205. status: () => ({
  206. json: payload => {
  207. expect(payload.error.code).to.equal('user_not_found')
  208. done()
  209. },
  210. }),
  211. })
  212. })
  213. it('handle invalid email', function (done) {
  214. this.req.body.email = 'not_valid_email'
  215. this.UserMembershipController.add(this.req, {
  216. status: () => ({
  217. json: payload => {
  218. expect(payload.error.code).to.equal('invalid_email')
  219. done()
  220. },
  221. }),
  222. })
  223. })
  224. })
  225. describe('remove', function () {
  226. beforeEach(function () {
  227. this.req.params.userId = this.newUser._id
  228. this.req.entity = this.subscription
  229. this.req.entityConfig = EntityConfigs.groupManagers
  230. })
  231. it('remove user', function (done) {
  232. this.UserMembershipController.remove(this.req, {
  233. sendStatus: () => {
  234. sinon.assert.calledWithMatch(
  235. this.UserMembershipHandler.removeUser,
  236. this.subscription,
  237. { modelName: 'Subscription' },
  238. this.newUser._id
  239. )
  240. done()
  241. },
  242. })
  243. })
  244. it('handle readOnly entity', function (done) {
  245. this.req.entityConfig = EntityConfigs.group
  246. this.UserMembershipController.remove(this.req, null, error => {
  247. expect(error).to.exist
  248. expect(error).to.be.an.instanceof(Errors.NotFoundError)
  249. done()
  250. })
  251. })
  252. it('prevent self removal', function (done) {
  253. this.req.params.userId = this.user._id
  254. this.UserMembershipController.remove(this.req, {
  255. status: () => ({
  256. json: payload => {
  257. expect(payload.error.code).to.equal('managers_cannot_remove_self')
  258. done()
  259. },
  260. }),
  261. })
  262. })
  263. it('prevent admin removal', function (done) {
  264. this.UserMembershipHandler.removeUser.yields(new UserIsManagerError())
  265. this.UserMembershipController.remove(this.req, {
  266. status: () => ({
  267. json: payload => {
  268. expect(payload.error.code).to.equal('managers_cannot_remove_admin')
  269. done()
  270. },
  271. }),
  272. })
  273. })
  274. })
  275. describe('exportCsv', function () {
  276. beforeEach(function () {
  277. this.req.entity = this.subscription
  278. this.req.entityConfig = EntityConfigs.groupManagers
  279. this.res = new MockResponse()
  280. this.UserMembershipController.exportCsv(this.req, this.res)
  281. })
  282. it('get users', function () {
  283. sinon.assert.calledWithMatch(
  284. this.UserMembershipHandler.getUsers,
  285. this.subscription,
  286. { modelName: 'Subscription' }
  287. )
  288. })
  289. it('should set the correct content type on the request', function () {
  290. assertCalledWith(this.res.contentType, 'text/csv; charset=utf-8')
  291. })
  292. it('should name the exported csv file', function () {
  293. assertCalledWith(
  294. this.res.header,
  295. 'Content-Disposition',
  296. 'attachment; filename="Group.csv"'
  297. )
  298. })
  299. it('should export the correct csv', function () {
  300. assertCalledWith(
  301. this.res.send,
  302. '"email","last_logged_in_at","last_active_at"\n"mock-email-1@foo.com","2020-08-09T12:43:11.467Z","2021-08-09T12:43:11.467Z"\n"mock-email-2@foo.com","2020-05-20T10:41:11.407Z","2021-05-20T10:41:11.407Z"'
  303. )
  304. })
  305. })
  306. describe('new', function () {
  307. beforeEach(function () {
  308. this.req.params.name = 'publisher'
  309. this.req.params.id = 'abc'
  310. })
  311. it('renders view', function (done) {
  312. this.UserMembershipController.new(this.req, {
  313. render: (viewPath, data) => {
  314. expect(data.entityName).to.eq('publisher')
  315. expect(data.entityId).to.eq('abc')
  316. done()
  317. },
  318. })
  319. })
  320. })
  321. describe('create', function () {
  322. beforeEach(function () {
  323. this.req.params.name = 'institution'
  324. this.req.entityConfig = EntityConfigs.institution
  325. this.req.params.id = 123
  326. })
  327. it('creates institution', function (done) {
  328. this.UserMembershipController.create(this.req, {
  329. redirect: path => {
  330. expect(path).to.eq(EntityConfigs.institution.pathsFor(123).index)
  331. sinon.assert.calledWithMatch(
  332. this.UserMembershipHandler.createEntity,
  333. 123,
  334. { modelName: 'Institution' }
  335. )
  336. done()
  337. },
  338. })
  339. })
  340. })
  341. })