InstitutionsManagerTests.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. const SandboxedModule = require('sandboxed-module')
  2. const path = require('path')
  3. const sinon = require('sinon')
  4. const { expect } = require('chai')
  5. const modulePath = path.join(
  6. __dirname,
  7. '../../../../app/src/Features/Institutions/InstitutionsManager'
  8. )
  9. const Features = require('../../../../app/src/infrastructure/Features')
  10. describe('InstitutionsManager', function () {
  11. beforeEach(function () {
  12. this.institutionId = 123
  13. this.user = {}
  14. this.getInstitutionAffiliations = sinon.stub()
  15. this.refreshFeatures = sinon.stub().yields()
  16. this.users = [
  17. { _id: 'lapsed', features: {} },
  18. { _id: '1a', features: {} },
  19. { _id: '2b', features: {} },
  20. { _id: '3c', features: {} },
  21. ]
  22. this.ssoUsers = [
  23. {
  24. _id: '1a',
  25. samlIdentifiers: [{ providerId: this.institutionId.toString() }],
  26. },
  27. {
  28. _id: '2b',
  29. samlIdentifiers: [
  30. {
  31. providerId: this.institutionId.toString(),
  32. hasEntitlement: true,
  33. },
  34. ],
  35. },
  36. {
  37. _id: 'lapsed',
  38. samlIdentifiers: [{ providerId: this.institutionId.toString() }],
  39. hasEntitlement: true,
  40. },
  41. ]
  42. this.UserGetter = {
  43. getUsersByAnyConfirmedEmail: sinon.stub().yields(),
  44. getUser: sinon.stub().callsArgWith(1, null, this.user),
  45. promises: {
  46. getUsers: sinon.stub().resolves(this.users),
  47. getUsersByAnyConfirmedEmail: sinon.stub().resolves(),
  48. getSsoUsersAtInstitution: (this.getSsoUsersAtInstitution = sinon
  49. .stub()
  50. .resolves(this.ssoUsers)),
  51. },
  52. }
  53. this.creator = { create: sinon.stub().callsArg(0) }
  54. this.NotificationsBuilder = {
  55. featuresUpgradedByAffiliation: sinon.stub().returns(this.creator),
  56. redundantPersonalSubscription: sinon.stub().returns(this.creator),
  57. }
  58. this.SubscriptionLocator = {
  59. getUsersSubscription: sinon.stub().callsArg(1),
  60. }
  61. this.institutionWithV1Data = { name: 'Wombat University' }
  62. this.institution = {
  63. fetchV1Data: sinon
  64. .stub()
  65. .callsArgWith(0, null, this.institutionWithV1Data),
  66. }
  67. this.InstitutionModel = {
  68. Institution: {
  69. findOne: sinon.stub().callsArgWith(1, null, this.institution),
  70. },
  71. }
  72. this.subscriptionExec = sinon.stub().yields()
  73. const SubscriptionModel = {
  74. Subscription: {
  75. find: () => ({
  76. populate: () => ({
  77. exec: this.subscriptionExec,
  78. }),
  79. }),
  80. },
  81. }
  82. this.Mongo = { ObjectId: sinon.stub().returnsArg(0) }
  83. this.v1Counts = {
  84. user_ids: this.users.map(user => user._id),
  85. current_users_count: 3,
  86. lapsed_user_ids: ['lapsed'],
  87. entitled_via_sso: 1, // 2 entitled, but 1 lapsed
  88. with_confirmed_email: 2, // 1 non entitled SSO + 1 email user
  89. }
  90. this.InstitutionsManager = SandboxedModule.require(modulePath, {
  91. requires: {
  92. './InstitutionsAPI': {
  93. getInstitutionAffiliations: this.getInstitutionAffiliations,
  94. promises: {
  95. getInstitutionAffiliations:
  96. (this.getInstitutionAffiliationsPromise = sinon
  97. .stub()
  98. .resolves(this.affiliations)),
  99. getInstitutionAffiliationsCounts:
  100. (this.getInstitutionAffiliationsCounts = sinon
  101. .stub()
  102. .resolves(this.v1Counts)),
  103. },
  104. },
  105. '../Subscription/FeaturesUpdater': {
  106. refreshFeatures: this.refreshFeatures,
  107. },
  108. '../Subscription/FeaturesHelper': {
  109. isFeatureSetBetter: (this.isFeatureSetBetter = sinon.stub()),
  110. },
  111. '../User/UserGetter': this.UserGetter,
  112. '../Notifications/NotificationsBuilder': this.NotificationsBuilder,
  113. '../Subscription/SubscriptionLocator': this.SubscriptionLocator,
  114. '../../models/Institution': this.InstitutionModel,
  115. '../../models/Subscription': SubscriptionModel,
  116. mongodb: this.Mongo,
  117. },
  118. })
  119. })
  120. describe('refreshInstitutionUsers', function () {
  121. beforeEach(function () {
  122. this.user1Id = '123abc123abc123abc123abc'
  123. this.user2Id = '456def456def456def456def'
  124. this.user3Id = 'trial123abc'
  125. this.user4Id = 'group123abc'
  126. this.affiliations = [
  127. { user_id: this.user1Id },
  128. { user_id: this.user2Id },
  129. { user_id: this.user3Id },
  130. { user_id: this.user4Id },
  131. ]
  132. this.user1 = { _id: this.user1Id }
  133. this.user2 = { _id: this.user2Id }
  134. this.user3 = { _id: this.user3Id }
  135. this.user4 = { _id: this.user4Id }
  136. this.UserGetter.getUser
  137. .withArgs(this.user1Id)
  138. .callsArgWith(1, null, this.user1)
  139. this.UserGetter.getUser
  140. .withArgs(this.user2Id)
  141. .callsArgWith(1, null, this.user2)
  142. this.UserGetter.getUser
  143. .withArgs(this.user3Id)
  144. .callsArgWith(1, null, this.user3)
  145. this.UserGetter.getUser
  146. .withArgs(this.user4Id)
  147. .callsArgWith(1, null, this.user4)
  148. this.SubscriptionLocator.getUsersSubscription
  149. .withArgs(this.user2)
  150. .callsArgWith(1, null, {
  151. planCode: 'pro',
  152. groupPlan: false,
  153. })
  154. this.SubscriptionLocator.getUsersSubscription
  155. .withArgs(this.user3)
  156. .callsArgWith(1, null, {
  157. planCode: 'collaborator_free_trial_7_days',
  158. groupPlan: false,
  159. })
  160. this.SubscriptionLocator.getUsersSubscription
  161. .withArgs(this.user4)
  162. .callsArgWith(1, null, {
  163. planCode: 'collaborator-annual',
  164. groupPlan: true,
  165. })
  166. this.refreshFeatures.withArgs(this.user1Id).yields(null, {}, true)
  167. this.getInstitutionAffiliations.yields(null, this.affiliations)
  168. })
  169. it('refresh all users Features', function (done) {
  170. this.InstitutionsManager.refreshInstitutionUsers(
  171. this.institutionId,
  172. false,
  173. error => {
  174. expect(error).not.to.exist
  175. sinon.assert.callCount(this.refreshFeatures, 4)
  176. // expect no notifications
  177. sinon.assert.notCalled(
  178. this.NotificationsBuilder.featuresUpgradedByAffiliation
  179. )
  180. sinon.assert.notCalled(
  181. this.NotificationsBuilder.redundantPersonalSubscription
  182. )
  183. done()
  184. }
  185. )
  186. })
  187. it('notifies users if their features have been upgraded', function (done) {
  188. this.InstitutionsManager.refreshInstitutionUsers(
  189. this.institutionId,
  190. true,
  191. error => {
  192. expect(error).not.to.exist
  193. sinon.assert.calledOnce(
  194. this.NotificationsBuilder.featuresUpgradedByAffiliation
  195. )
  196. sinon.assert.calledWith(
  197. this.NotificationsBuilder.featuresUpgradedByAffiliation,
  198. this.affiliations[0],
  199. this.user1
  200. )
  201. done()
  202. }
  203. )
  204. })
  205. it('notifies users if they have a subscription, or a trial subscription, that should be cancelled', function (done) {
  206. this.InstitutionsManager.refreshInstitutionUsers(
  207. this.institutionId,
  208. true,
  209. error => {
  210. expect(error).not.to.exist
  211. sinon.assert.calledTwice(
  212. this.NotificationsBuilder.redundantPersonalSubscription
  213. )
  214. sinon.assert.calledWith(
  215. this.NotificationsBuilder.redundantPersonalSubscription,
  216. this.affiliations[1],
  217. this.user2
  218. )
  219. sinon.assert.calledWith(
  220. this.NotificationsBuilder.redundantPersonalSubscription,
  221. this.affiliations[2],
  222. this.user3
  223. )
  224. done()
  225. }
  226. )
  227. })
  228. })
  229. describe('checkInstitutionUsers', function () {
  230. it('returns entitled/not, sso/not, lapsed/current, and pro counts', async function () {
  231. if (Features.hasFeature('saas')) {
  232. this.isFeatureSetBetter.returns(true)
  233. const usersSummary =
  234. await this.InstitutionsManager.promises.checkInstitutionUsers(
  235. this.institutionId
  236. )
  237. expect(usersSummary).to.deep.equal({
  238. emailUsers: {
  239. total: 1,
  240. current: 1,
  241. lapsed: 0,
  242. pro: {
  243. current: 1, // isFeatureSetBetter stubbed to return true for all
  244. lapsed: 0,
  245. },
  246. nonPro: {
  247. current: 0,
  248. lapsed: 0,
  249. },
  250. },
  251. ssoUsers: {
  252. total: 3,
  253. lapsed: 1,
  254. current: {
  255. entitled: 1,
  256. notEntitled: 1,
  257. },
  258. pro: {
  259. current: 2,
  260. lapsed: 1, // isFeatureSetBetter stubbed to return true for all users
  261. },
  262. nonPro: {
  263. current: 0,
  264. lapsed: 0,
  265. },
  266. },
  267. })
  268. }
  269. })
  270. it('includes withConfirmedEmailMismatch when v1 and v2 counts do not add up', async function () {
  271. if (Features.hasFeature('saas')) {
  272. this.isFeatureSetBetter.returns(true)
  273. this.v1Counts.with_confirmed_email = 100
  274. const usersSummary =
  275. await this.InstitutionsManager.promises.checkInstitutionUsers(
  276. this.institutionId
  277. )
  278. expect(usersSummary).to.deep.equal({
  279. emailUsers: {
  280. total: 1,
  281. current: 1,
  282. lapsed: 0,
  283. pro: {
  284. current: 1, // isFeatureSetBetter stubbed to return true for all
  285. lapsed: 0,
  286. },
  287. nonPro: {
  288. current: 0,
  289. lapsed: 0,
  290. },
  291. },
  292. ssoUsers: {
  293. total: 3,
  294. lapsed: 1,
  295. current: {
  296. entitled: 1,
  297. notEntitled: 1,
  298. },
  299. pro: {
  300. current: 2,
  301. lapsed: 1, // isFeatureSetBetter stubbed to return true for all users
  302. },
  303. nonPro: {
  304. current: 0,
  305. lapsed: 0,
  306. },
  307. },
  308. databaseMismatch: {
  309. withConfirmedEmail: {
  310. v1: 100,
  311. v2: 2,
  312. },
  313. },
  314. })
  315. }
  316. })
  317. })
  318. describe('getInstitutionUsersSubscriptions', function () {
  319. it('returns all institution users subscriptions', function (done) {
  320. const stubbedUsers = [
  321. { user_id: '123abc123abc123abc123abc' },
  322. { user_id: '456def456def456def456def' },
  323. { user_id: '789def789def789def789def' },
  324. ]
  325. this.getInstitutionAffiliations.yields(null, stubbedUsers)
  326. this.InstitutionsManager.getInstitutionUsersSubscriptions(
  327. this.institutionId,
  328. (error, subscriptions) => {
  329. expect(error).not.to.exist
  330. sinon.assert.calledOnce(this.subscriptionExec)
  331. done()
  332. }
  333. )
  334. })
  335. })
  336. })