ProjectControllerTests.coffee 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. should = require('chai').should()
  2. SandboxedModule = require('sandboxed-module')
  3. assert = require('assert')
  4. path = require('path')
  5. sinon = require('sinon')
  6. modulePath = path.join __dirname, "../../../../app/js/Features/Project/ProjectController"
  7. expect = require("chai").expect
  8. describe "ProjectController", ->
  9. beforeEach ->
  10. @project_id = "123213jlkj9kdlsaj"
  11. @user =
  12. _id:"588f3ddae8ebc1bac07c9fa4"
  13. first_name: "bjkdsjfk"
  14. features: {}
  15. @settings =
  16. apis:
  17. chat:
  18. url:"chat.com"
  19. siteUrl: "mysite.com"
  20. @token = 'some-token'
  21. @ProjectDeleter =
  22. archiveProject: sinon.stub().callsArg(1)
  23. deleteProject: sinon.stub().callsArg(1)
  24. restoreProject: sinon.stub().callsArg(1)
  25. findArchivedProjects: sinon.stub()
  26. @ProjectDuplicator =
  27. duplicate: sinon.stub().callsArgWith(3, null, {_id:@project_id})
  28. @ProjectCreationHandler =
  29. createExampleProject: sinon.stub().callsArgWith(2, null, {_id:@project_id})
  30. createBasicProject: sinon.stub().callsArgWith(2, null, {_id:@project_id})
  31. @SubscriptionLocator =
  32. getUsersSubscription: sinon.stub()
  33. @LimitationsManager =
  34. userHasSubscriptionOrIsGroupMember: sinon.stub()
  35. @TagsHandler =
  36. getAllTags: sinon.stub()
  37. @NotificationsHandler =
  38. getUserNotifications: sinon.stub()
  39. @UserModel =
  40. findById: sinon.stub()
  41. @AuthorizationManager =
  42. getPrivilegeLevelForProject:sinon.stub()
  43. @EditorController =
  44. renameProject:sinon.stub()
  45. @InactiveProjectManager =
  46. reactivateProjectIfRequired:sinon.stub()
  47. @ProjectUpdateHandler =
  48. markAsOpened: sinon.stub()
  49. @ReferencesSearchHandler =
  50. indexProjectReferences: sinon.stub()
  51. @ProjectGetter =
  52. findAllUsersProjects: sinon.stub()
  53. getProject: sinon.stub()
  54. @AuthenticationController =
  55. getLoggedInUser: sinon.stub().callsArgWith(1, null, @user)
  56. getLoggedInUserId: sinon.stub().returns(@user._id)
  57. getSessionUser: sinon.stub().returns(@user)
  58. isUserLoggedIn: sinon.stub().returns(true)
  59. @AnalyticsManager =
  60. getLastOccurrence: sinon.stub()
  61. @TokenAccessHandler =
  62. getRequestToken: sinon.stub().returns(@token)
  63. protectTokens: sinon.stub()
  64. @CollaboratorsHandler =
  65. userIsTokenMember: sinon.stub().callsArgWith(2, null, false)
  66. @Modules =
  67. hooks:
  68. fire: sinon.stub()
  69. @ProjectController = SandboxedModule.require modulePath, requires:
  70. "settings-sharelatex":@settings
  71. "logger-sharelatex":
  72. log:->
  73. err:->
  74. "metrics-sharelatex":
  75. Timer:->
  76. done:->
  77. inc:->
  78. "./ProjectDeleter": @ProjectDeleter
  79. "./ProjectDuplicator": @ProjectDuplicator
  80. "./ProjectCreationHandler": @ProjectCreationHandler
  81. "../Editor/EditorController": @EditorController
  82. "../Subscription/SubscriptionLocator": @SubscriptionLocator
  83. "../Subscription/LimitationsManager": @LimitationsManager
  84. "../Tags/TagsHandler":@TagsHandler
  85. "../Notifications/NotificationsHandler":@NotificationsHandler
  86. "../../models/User":User:@UserModel
  87. "../Authorization/AuthorizationManager":@AuthorizationManager
  88. "../InactiveData/InactiveProjectManager":@InactiveProjectManager
  89. "./ProjectUpdateHandler":@ProjectUpdateHandler
  90. "../ReferencesSearch/ReferencesSearchHandler": @ReferencesSearchHandler
  91. "./ProjectGetter": @ProjectGetter
  92. '../Authentication/AuthenticationController': @AuthenticationController
  93. "../Analytics/AnalyticsManager": @AnalyticsManager
  94. "../TokenAccess/TokenAccessHandler": @TokenAccessHandler
  95. "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler
  96. "../../infrastructure/Modules": @Modules
  97. @projectName = "£12321jkj9ujkljds"
  98. @req =
  99. params:
  100. Project_id: @project_id
  101. session:
  102. user: @user
  103. body:
  104. projectName: @projectName
  105. i18n:
  106. translate:->
  107. @res =
  108. locals:
  109. jsPath:"js path here"
  110. describe "updateProjectSettings", ->
  111. it "should update the name", (done) ->
  112. @EditorController.renameProject = sinon.stub().callsArg(2)
  113. @req.body =
  114. name: @name = "New name"
  115. @res.sendStatus = (code) =>
  116. @EditorController.renameProject
  117. .calledWith(@project_id, @name)
  118. .should.equal true
  119. code.should.equal 204
  120. done()
  121. @ProjectController.updateProjectSettings @req, @res
  122. it "should update the compiler", (done) ->
  123. @EditorController.setCompiler = sinon.stub().callsArg(2)
  124. @req.body =
  125. compiler: @compiler = "pdflatex"
  126. @res.sendStatus = (code) =>
  127. @EditorController.setCompiler
  128. .calledWith(@project_id, @compiler)
  129. .should.equal true
  130. code.should.equal 204
  131. done()
  132. @ProjectController.updateProjectSettings @req, @res
  133. it "should update the spell check language", (done) ->
  134. @EditorController.setSpellCheckLanguage = sinon.stub().callsArg(2)
  135. @req.body =
  136. spellCheckLanguage: @languageCode = "fr"
  137. @res.sendStatus = (code) =>
  138. @EditorController.setSpellCheckLanguage
  139. .calledWith(@project_id, @languageCode)
  140. .should.equal true
  141. code.should.equal 204
  142. done()
  143. @ProjectController.updateProjectSettings @req, @res
  144. it "should update the root doc", (done) ->
  145. @EditorController.setRootDoc = sinon.stub().callsArg(2)
  146. @req.body =
  147. rootDocId: @rootDocId = "root-doc-id"
  148. @res.sendStatus = (code) =>
  149. @EditorController.setRootDoc
  150. .calledWith(@project_id, @rootDocId)
  151. .should.equal true
  152. code.should.equal 204
  153. done()
  154. @ProjectController.updateProjectSettings @req, @res
  155. describe "updateProjectAdminSettings", ->
  156. it "should update the public access level", (done) ->
  157. @EditorController.setPublicAccessLevel = sinon.stub().callsArg(2)
  158. @req.body =
  159. publicAccessLevel: @publicAccessLevel = "readonly"
  160. @res.sendStatus = (code) =>
  161. @EditorController.setPublicAccessLevel
  162. .calledWith(@project_id, @publicAccessLevel)
  163. .should.equal true
  164. code.should.equal 204
  165. done()
  166. @ProjectController.updateProjectAdminSettings @req, @res
  167. describe "deleteProject", ->
  168. it "should tell the project deleter to archive when forever=false", (done)->
  169. @res.sendStatus = (code)=>
  170. @ProjectDeleter.archiveProject.calledWith(@project_id).should.equal true
  171. code.should.equal 200
  172. done()
  173. @ProjectController.deleteProject @req, @res
  174. it "should tell the project deleter to delete when forever=true", (done)->
  175. @req.query = forever: "true"
  176. @res.sendStatus = (code)=>
  177. @ProjectDeleter.deleteProject.calledWith(@project_id).should.equal true
  178. code.should.equal 200
  179. done()
  180. @ProjectController.deleteProject @req, @res
  181. describe "restoreProject", ->
  182. it "should tell the project deleter", (done)->
  183. @res.sendStatus = (code)=>
  184. @ProjectDeleter.restoreProject.calledWith(@project_id).should.equal true
  185. code.should.equal 200
  186. done()
  187. @ProjectController.restoreProject @req, @res
  188. describe "cloneProject", ->
  189. it "should call the project duplicator", (done)->
  190. @res.send = (json)=>
  191. @ProjectDuplicator.duplicate.calledWith(@user, @project_id, @projectName).should.equal true
  192. json.project_id.should.equal @project_id
  193. done()
  194. @ProjectController.cloneProject @req, @res
  195. describe "newProject", ->
  196. it "should call the projectCreationHandler with createExampleProject", (done)->
  197. @req.body.template = "example"
  198. @res.send = (json)=>
  199. @ProjectCreationHandler.createExampleProject.calledWith(@user._id, @projectName).should.equal true
  200. @ProjectCreationHandler.createBasicProject.called.should.equal false
  201. done()
  202. @ProjectController.newProject @req, @res
  203. it "should call the projectCreationHandler with createBasicProject", (done)->
  204. @req.body.template = "basic"
  205. @res.send = (json)=>
  206. @ProjectCreationHandler.createExampleProject.called.should.equal false
  207. @ProjectCreationHandler.createBasicProject.calledWith(@user._id, @projectName).should.equal true
  208. done()
  209. @ProjectController.newProject @req, @res
  210. describe "projectListPage", ->
  211. beforeEach ->
  212. @tags = [{name:1, project_ids:["1","2","3"]}, {name:2, project_ids:["a","1"]}, {name:3, project_ids:["a", "b", "c", "d"]}]
  213. @notifications = [{_id:'1',user_id:'2',templateKey:'3',messageOpts:'4',key:'5'}]
  214. @projects = [
  215. {_id:1, lastUpdated:1, owner_ref: "user-1"},
  216. {_id:2, lastUpdated:2, owner_ref: "user-2"}
  217. ]
  218. @collabertions = [
  219. {_id:5, lastUpdated:5, owner_ref: "user-1"}
  220. ]
  221. @readOnly = [
  222. {_id:3, lastUpdated:3, owner_ref: "user-1"}
  223. ]
  224. @tokenReadAndWrite = [
  225. {_id:6, lastUpdated:5, owner_ref: "user-4"}
  226. ]
  227. @tokenReadOnly = [
  228. {_id:7, lastUpdated:4, owner_ref: "user-5"}
  229. ]
  230. @allProjects = {
  231. owned: @projects,
  232. readAndWrite: @collabertions,
  233. readOnly: @readOnly,
  234. tokenReadAndWrite: @tokenReadAndWrite,
  235. tokenReadOnly: @tokenReadOnly
  236. }
  237. @users =
  238. 'user-1':
  239. first_name: 'James'
  240. 'user-2':
  241. first_name: 'Henry'
  242. @users[@user._id] = @user # Owner
  243. @UserModel.findById = (id, fields, callback) =>
  244. callback null, @users[id]
  245. @LimitationsManager.userHasSubscriptionOrIsGroupMember.callsArgWith(1, null, false)
  246. @TagsHandler.getAllTags.callsArgWith(1, null, @tags, {})
  247. @NotificationsHandler.getUserNotifications = sinon.stub().callsArgWith(1, null, @notifications, {})
  248. @ProjectGetter.findAllUsersProjects.callsArgWith(2, null, @allProjects)
  249. @Modules.hooks.fire.withArgs('findAllV1Projects', @user._id).yields(undefined) # Without integration module hook, cb returns undefined
  250. it "should render the project/list page", (done)->
  251. @res.render = (pageName, opts)=>
  252. pageName.should.equal "project/list"
  253. done()
  254. @ProjectController.projectListPage @req, @res
  255. it "should send the tags", (done)->
  256. @res.render = (pageName, opts)=>
  257. opts.tags.length.should.equal @tags.length
  258. done()
  259. @ProjectController.projectListPage @req, @res
  260. it "should send the projects", (done)->
  261. @res.render = (pageName, opts)=>
  262. opts.projects.length.should.equal (@projects.length + @collabertions.length + @readOnly.length + @tokenReadAndWrite.length + @tokenReadOnly.length)
  263. done()
  264. @ProjectController.projectListPage @req, @res
  265. it "should send the user", (done)->
  266. @res.render = (pageName, opts)=>
  267. opts.user.should.deep.equal @user
  268. done()
  269. @ProjectController.projectListPage @req, @res
  270. it "should inject the users", (done) ->
  271. @res.render = (pageName, opts)=>
  272. opts.projects[0].owner.should.equal (@users[@projects[0].owner_ref])
  273. opts.projects[1].owner.should.equal (@users[@projects[1].owner_ref])
  274. done()
  275. @ProjectController.projectListPage @req, @res
  276. describe 'front widget', (done) ->
  277. beforeEach ->
  278. @settings.overleaf =
  279. front_chat_widget_room_id: 'chat-room-id'
  280. it 'should show for paid users', (done) ->
  281. @user.features.github = true
  282. @user.features.dropbox = true
  283. @res.render = (pageName, opts)=>
  284. opts.frontChatWidgetRoomId.should.equal @settings.overleaf.front_chat_widget_room_id
  285. done()
  286. @ProjectController.projectListPage @req, @res
  287. it 'should show for sample users', (done) ->
  288. @user._id = '588f3ddae8ebc1bac07c9f00' # last two digits
  289. @res.render = (pageName, opts)=>
  290. opts.frontChatWidgetRoomId.should.equal @settings.overleaf.front_chat_widget_room_id
  291. done()
  292. @ProjectController.projectListPage @req, @res
  293. it 'should not show for non sample users', (done) ->
  294. @user._id = '588f3ddae8ebc1bac07c9fff' # last two digits
  295. @res.render = (pageName, opts)=>
  296. expect(opts.frontChatWidgetRoomId).to.equal undefined
  297. done()
  298. @ProjectController.projectListPage @req, @res
  299. describe 'with overleaf-integration-web-module hook', ->
  300. beforeEach ->
  301. @V1Response =
  302. projects: [
  303. { id: '123mockV1Id', title: 'mock title', updated_at: 1509616411, removed: false, archived: false }
  304. { id: '456mockV1Id', title: 'mock title 2', updated_at: 1509616411, removed: true, archived: false }
  305. ],
  306. tags: [
  307. { name: 'mock tag', project_ids: ['123mockV1Id'] }
  308. ]
  309. @Modules.hooks.fire.withArgs('findAllV1Projects', @user._id).yields(null, [@V1Response]) # Need to wrap response in array, as multiple hooks could fire
  310. it 'should include V1 projects', (done) ->
  311. @res.render = (pageName, opts) =>
  312. opts.projects.length.should.equal (
  313. @projects.length +
  314. @collabertions.length +
  315. @readOnly.length +
  316. @tokenReadAndWrite.length +
  317. @tokenReadOnly.length +
  318. @V1Response.projects.length
  319. )
  320. opts.projects.forEach (p) ->
  321. # Check properties correctly mapped from V1
  322. expect(p).to.have.property 'id'
  323. expect(p).to.have.property 'name'
  324. expect(p).to.have.property 'lastUpdated'
  325. expect(p).to.have.property 'accessLevel'
  326. expect(p).to.have.property 'archived'
  327. done()
  328. @ProjectController.projectListPage @req, @res
  329. it 'should include V1 tags', (done) ->
  330. @res.render = (pageName, opts) =>
  331. opts.tags.length.should.equal (@tags.length + @V1Response.tags.length)
  332. opts.tags.forEach (t) ->
  333. expect(t).to.have.property 'name'
  334. expect(t).to.have.property 'project_ids'
  335. done()
  336. @ProjectController.projectListPage @req, @res
  337. it 'should have isShowingV1Projects flag', (done) ->
  338. @res.render = (pageName, opts) =>
  339. opts.isShowingV1Projects.should.equal true
  340. done()
  341. @ProjectController.projectListPage @req, @res
  342. describe "projectListPage with duplicate projects", ->
  343. beforeEach ->
  344. @tags = [{name:1, project_ids:["1","2","3"]}, {name:2, project_ids:["a","1"]}, {name:3, project_ids:["a", "b", "c", "d"]}]
  345. @notifications = [{_id:'1',user_id:'2',templateKey:'3',messageOpts:'4',key:'5'}]
  346. @projects = [
  347. {_id:1, lastUpdated:1, owner_ref: "user-1"},
  348. {_id:2, lastUpdated:2, owner_ref: "user-2"}
  349. ]
  350. @collabertions = [
  351. {_id:5, lastUpdated:5, owner_ref: "user-1"}
  352. ]
  353. @readOnly = [
  354. {_id:3, lastUpdated:3, owner_ref: "user-1"}
  355. ]
  356. @tokenReadAndWrite = [
  357. {_id:6, lastUpdated:5, owner_ref: "user-4"}
  358. ]
  359. @tokenReadOnly = [
  360. {_id:6, lastUpdated:5, owner_ref: "user-4"} # Also in tokenReadAndWrite
  361. {_id:7, lastUpdated:4, owner_ref: "user-5"}
  362. ]
  363. @allProjects = {
  364. owned: @projects,
  365. readAndWrite: @collabertions,
  366. readOnly: @readOnly,
  367. tokenReadAndWrite: @tokenReadAndWrite,
  368. tokenReadOnly: @tokenReadOnly
  369. }
  370. @users =
  371. 'user-1':
  372. first_name: 'James'
  373. 'user-2':
  374. first_name: 'Henry'
  375. @users[@user._id] = @user # Owner
  376. @UserModel.findById = (id, fields, callback) =>
  377. callback null, @users[id]
  378. @LimitationsManager.userHasSubscriptionOrIsGroupMember.callsArgWith(1, null, false)
  379. @TagsHandler.getAllTags.callsArgWith(1, null, @tags, {})
  380. @NotificationsHandler.getUserNotifications = sinon.stub().callsArgWith(1, null, @notifications, {})
  381. @ProjectGetter.findAllUsersProjects.callsArgWith(2, null, @allProjects)
  382. @Modules.hooks.fire.withArgs('findAllV1Projects', @user._id).yields(undefined) # Without integration module hook, cb returns undefined
  383. it "should render the project/list page", (done)->
  384. @res.render = (pageName, opts)=>
  385. pageName.should.equal "project/list"
  386. done()
  387. @ProjectController.projectListPage @req, @res
  388. it "should omit one of the projects", (done)->
  389. @res.render = (pageName, opts)=>
  390. opts.projects.length.should.equal (
  391. @projects.length + @collabertions.length + @readOnly.length + @tokenReadAndWrite.length + @tokenReadOnly.length - 1
  392. )
  393. done()
  394. @ProjectController.projectListPage @req, @res
  395. describe "renameProject", ->
  396. beforeEach ->
  397. @newProjectName = "my supper great new project"
  398. @req.body.newProjectName = @newProjectName
  399. it "should call the editor controller", (done)->
  400. @EditorController.renameProject.callsArgWith(2)
  401. @res.sendStatus = (code)=>
  402. code.should.equal 200
  403. @EditorController.renameProject.calledWith(@project_id, @newProjectName).should.equal true
  404. done()
  405. @ProjectController.renameProject @req, @res
  406. it "should send an error to next() if there is a problem", (done)->
  407. @EditorController.renameProject.callsArgWith(2, error = new Error("problem"))
  408. next = (e)=>
  409. e.should.equal error
  410. done()
  411. @ProjectController.renameProject @req, @res, next
  412. describe "loadEditor", ->
  413. beforeEach ->
  414. @settings.editorIsOpen = true
  415. @project =
  416. name:"my proj"
  417. _id:"213123kjlkj"
  418. owner_ref: '59fc84d5fbea77482d436e1b'
  419. @user =
  420. _id: "588f3ddae8ebc1bac07c9fa4"
  421. ace:
  422. fontSize:"massive"
  423. theme:"sexy"
  424. email: "bob@bob.com"
  425. @ProjectGetter.getProject.callsArgWith 2, null, @project
  426. @UserModel.findById.callsArgWith(1, null, @user)
  427. @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {})
  428. @AuthorizationManager.getPrivilegeLevelForProject.callsArgWith 3, null, "owner"
  429. @ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub()
  430. @InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1)
  431. @AnalyticsManager.getLastOccurrence.yields(null, {"mock": "event"})
  432. @ProjectUpdateHandler.markAsOpened.callsArgWith(1)
  433. it "should render the project/editor page", (done)->
  434. @res.render = (pageName, opts)=>
  435. pageName.should.equal "project/editor"
  436. done()
  437. @ProjectController.loadEditor @req, @res
  438. it "should add user", (done)->
  439. @res.render = (pageName, opts)=>
  440. opts.user.email.should.equal @user.email
  441. done()
  442. @ProjectController.loadEditor @req, @res
  443. it "should add on userSettings", (done)->
  444. @res.render = (pageName, opts)=>
  445. opts.userSettings.fontSize.should.equal @user.ace.fontSize
  446. opts.userSettings.theme.should.equal @user.ace.theme
  447. done()
  448. @ProjectController.loadEditor @req, @res
  449. it "should render the closed page if the editor is closed", (done)->
  450. @settings.editorIsOpen = false
  451. @res.render = (pageName, opts)=>
  452. pageName.should.equal "general/closed"
  453. done()
  454. @ProjectController.loadEditor @req, @res
  455. it "should not render the page if the project can not be accessed", (done)->
  456. @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub().callsArgWith 3, null, null
  457. @res.sendStatus = (resCode, opts)=>
  458. resCode.should.equal 401
  459. done()
  460. @ProjectController.loadEditor @req, @res
  461. it "should reactivateProjectIfRequired", (done)->
  462. @res.render = (pageName, opts)=>
  463. @InactiveProjectManager.reactivateProjectIfRequired.calledWith(@project_id).should.equal true
  464. done()
  465. @ProjectController.loadEditor @req, @res
  466. it "should mark project as opened", (done)->
  467. @res.render = (pageName, opts)=>
  468. @ProjectUpdateHandler.markAsOpened.calledWith(@project_id).should.equal true
  469. done()
  470. @ProjectController.loadEditor @req, @res
  471. describe '_isInPercentageRollout', ->
  472. before ->
  473. @ids = [
  474. '5a05cd7621f9fe22be131740',
  475. '5a05cd7821f9fe22be131741',
  476. '5a05cd7921f9fe22be131742',
  477. '5a05cd7a21f9fe22be131743',
  478. '5a05cd7b21f9fe22be131744',
  479. '5a05cd7c21f9fe22be131745',
  480. '5a05cd7d21f9fe22be131746',
  481. '5a05cd7e21f9fe22be131747',
  482. '5a05cd7f21f9fe22be131748',
  483. '5a05cd8021f9fe22be131749',
  484. '5a05cd8021f9fe22be13174a',
  485. '5a05cd8121f9fe22be13174b',
  486. '5a05cd8221f9fe22be13174c',
  487. '5a05cd8221f9fe22be13174d',
  488. '5a05cd8321f9fe22be13174e',
  489. '5a05cd8321f9fe22be13174f',
  490. '5a05cd8421f9fe22be131750',
  491. '5a05cd8421f9fe22be131751',
  492. '5a05cd8421f9fe22be131752',
  493. '5a05cd8521f9fe22be131753'
  494. ]
  495. it 'should produce the expected results', ->
  496. expect(
  497. @ids.map (i) =>
  498. @ProjectController._isInPercentageRollout('abcd', i, 50)
  499. ).to.deep.equal [
  500. false,
  501. false,
  502. false,
  503. false,
  504. false,
  505. false,
  506. true,
  507. false,
  508. true,
  509. true,
  510. true,
  511. true,
  512. true,
  513. true,
  514. false,
  515. false,
  516. false,
  517. true,
  518. false,
  519. true
  520. ]
  521. expect(
  522. @ids.map (i) =>
  523. @ProjectController._isInPercentageRollout('efgh', i, 50)
  524. ).to.deep.equal [
  525. false,
  526. false,
  527. false,
  528. false,
  529. true,
  530. false,
  531. false,
  532. true,
  533. false,
  534. false,
  535. true,
  536. true,
  537. true,
  538. false,
  539. true,
  540. false,
  541. true,
  542. true,
  543. false,
  544. false
  545. ]