ProjectControllerTests.js 64 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944
  1. const SandboxedModule = require('sandboxed-module')
  2. const path = require('path')
  3. const sinon = require('sinon')
  4. const { expect } = require('chai')
  5. const { ObjectId } = require('mongodb')
  6. const Errors = require('../../../../app/src/Features/Errors/Errors')
  7. const MODULE_PATH = path.join(
  8. __dirname,
  9. '../../../../app/src/Features/Project/ProjectController'
  10. )
  11. describe('ProjectController', function () {
  12. beforeEach(function () {
  13. this.project_id = ObjectId('abcdefabcdefabcdefabcdef')
  14. this.user = {
  15. _id: ObjectId('123456123456123456123456'),
  16. email: 'test@overleaf.com',
  17. first_name: 'bjkdsjfk',
  18. features: {},
  19. emails: [{ email: 'test@overleaf.com' }],
  20. }
  21. this.settings = {
  22. apis: {
  23. chat: {
  24. url: 'chat.com',
  25. },
  26. },
  27. siteUrl: 'https://overleaf.com',
  28. algolia: {},
  29. }
  30. this.brandVariationDetails = {
  31. id: '12',
  32. active: true,
  33. brand_name: 'The journal',
  34. home_url: 'http://www.thejournal.com/',
  35. publish_menu_link_html: 'Submit your paper to the <em>The Journal</em>',
  36. }
  37. this.token = 'some-token'
  38. this.ProjectDeleter = {
  39. deleteProject: sinon.stub().callsArg(2),
  40. restoreProject: sinon.stub().callsArg(1),
  41. findArchivedProjects: sinon.stub(),
  42. }
  43. this.ProjectDuplicator = {
  44. duplicate: sinon.stub().callsArgWith(3, null, { _id: this.project_id }),
  45. }
  46. this.ProjectCreationHandler = {
  47. createExampleProject: sinon
  48. .stub()
  49. .callsArgWith(2, null, { _id: this.project_id }),
  50. createBasicProject: sinon
  51. .stub()
  52. .callsArgWith(2, null, { _id: this.project_id }),
  53. }
  54. this.SubscriptionLocator = { getUsersSubscription: sinon.stub() }
  55. this.LimitationsManager = { hasPaidSubscription: sinon.stub() }
  56. this.TagsHandler = { getAllTags: sinon.stub() }
  57. this.NotificationsHandler = { getUserNotifications: sinon.stub() }
  58. this.UserModel = { findById: sinon.stub() }
  59. this.AuthorizationManager = {
  60. getPrivilegeLevelForProject: sinon.stub(),
  61. isRestrictedUser: sinon.stub().returns(false),
  62. }
  63. this.EditorController = { renameProject: sinon.stub() }
  64. this.InactiveProjectManager = { reactivateProjectIfRequired: sinon.stub() }
  65. this.ProjectUpdateHandler = { markAsOpened: sinon.stub() }
  66. this.ProjectGetter = {
  67. findAllUsersProjects: sinon.stub(),
  68. getProject: sinon.stub(),
  69. }
  70. this.ProjectHelper = {
  71. isArchived: sinon.stub(),
  72. isTrashed: sinon.stub(),
  73. isArchivedOrTrashed: sinon.stub(),
  74. getAllowedImagesForUser: sinon.stub().returns([]),
  75. }
  76. this.SessionManager = {
  77. getLoggedInUser: sinon.stub().callsArgWith(1, null, this.user),
  78. getLoggedInUserId: sinon.stub().returns(this.user._id),
  79. getSessionUser: sinon.stub().returns(this.user),
  80. isUserLoggedIn: sinon.stub().returns(true),
  81. }
  82. this.UserController = {
  83. logout: sinon.stub(),
  84. }
  85. this.TokenAccessHandler = {
  86. getRequestToken: sinon.stub().returns(this.token),
  87. protectTokens: sinon.stub(),
  88. }
  89. this.CollaboratorsGetter = {
  90. userIsTokenMember: sinon.stub().callsArgWith(2, null, false),
  91. }
  92. this.ProjectEntityHandler = {}
  93. this.NotificationBuilder = {
  94. ipMatcherAffiliation: sinon.stub().returns({ create: sinon.stub() }),
  95. }
  96. this.UserGetter = {
  97. getUserFullEmails: sinon.stub().yields(null, []),
  98. getUser: sinon
  99. .stub()
  100. .callsArgWith(2, null, { lastLoginIp: '192.170.18.2' }),
  101. }
  102. this.Features = {
  103. hasFeature: sinon.stub(),
  104. }
  105. this.FeaturesUpdater = {
  106. featuresEpochIsCurrent: sinon.stub().returns(true),
  107. refreshFeatures: sinon.stub().yields(null, this.user),
  108. }
  109. this.BrandVariationsHandler = {
  110. getBrandVariationById: sinon
  111. .stub()
  112. .callsArgWith(1, null, this.brandVariationDetails),
  113. }
  114. this.TpdsProjectFlusher = {
  115. flushProjectToTpdsIfNeeded: sinon.stub().yields(),
  116. }
  117. this.Metrics = {
  118. Timer: class {
  119. done() {}
  120. },
  121. inc: sinon.stub(),
  122. }
  123. this.NewLogsUIHelper = {
  124. getNewLogsUIVariantForUser: sinon
  125. .stub()
  126. .returns({ newLogsUI: false, subvariant: null }),
  127. }
  128. this.SplitTestHandler = {
  129. promises: {
  130. getTestSegmentation: sinon.stub().resolves({ enabled: false }),
  131. },
  132. getTestSegmentation: sinon.stub().yields(null, { enabled: false }),
  133. }
  134. this.SplitTestV2Handler = {
  135. promises: {
  136. getAssignment: sinon.stub().resolves({ active: false }),
  137. assignInLocalsContext: sinon.stub().resolves(),
  138. },
  139. getAssignment: sinon.stub().yields(null, { active: false }),
  140. assignInLocalsContext: sinon.stub().yields(null),
  141. }
  142. this.ProjectController = SandboxedModule.require(MODULE_PATH, {
  143. requires: {
  144. mongodb: { ObjectId },
  145. '@overleaf/settings': this.settings,
  146. '@overleaf/metrics': this.Metrics,
  147. '../SplitTests/SplitTestHandler': this.SplitTestHandler,
  148. '../SplitTests/SplitTestV2Handler': this.SplitTestV2Handler,
  149. './ProjectDeleter': this.ProjectDeleter,
  150. './ProjectDuplicator': this.ProjectDuplicator,
  151. './ProjectCreationHandler': this.ProjectCreationHandler,
  152. '../Editor/EditorController': this.EditorController,
  153. '../User/UserController': this.UserController,
  154. './ProjectHelper': this.ProjectHelper,
  155. '../Subscription/SubscriptionLocator': this.SubscriptionLocator,
  156. '../Subscription/LimitationsManager': this.LimitationsManager,
  157. '../Tags/TagsHandler': this.TagsHandler,
  158. '../Notifications/NotificationsHandler': this.NotificationsHandler,
  159. '../../models/User': { User: this.UserModel },
  160. '../Authorization/AuthorizationManager': this.AuthorizationManager,
  161. '../InactiveData/InactiveProjectManager': this.InactiveProjectManager,
  162. './ProjectUpdateHandler': this.ProjectUpdateHandler,
  163. './ProjectGetter': this.ProjectGetter,
  164. './ProjectDetailsHandler': this.ProjectDetailsHandler,
  165. '../Authentication/SessionManager': this.SessionManager,
  166. '../TokenAccess/TokenAccessHandler': this.TokenAccessHandler,
  167. '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
  168. './ProjectEntityHandler': this.ProjectEntityHandler,
  169. '../../infrastructure/Features': this.Features,
  170. '../Subscription/FeaturesUpdater': this.FeaturesUpdater,
  171. '../Notifications/NotificationsBuilder': this.NotificationBuilder,
  172. '../User/UserGetter': this.UserGetter,
  173. '../BrandVariations/BrandVariationsHandler': this
  174. .BrandVariationsHandler,
  175. '../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher,
  176. '../../models/Project': {},
  177. '../Analytics/AnalyticsManager': { recordEventForUser: () => {} },
  178. '../../infrastructure/Modules': {
  179. hooks: { fire: sinon.stub().yields(null, []) },
  180. },
  181. '../Helpers/NewLogsUI': this.NewLogsUIHelper,
  182. },
  183. })
  184. this.projectName = '£12321jkj9ujkljds'
  185. this.req = {
  186. query: {},
  187. params: {
  188. Project_id: this.project_id,
  189. },
  190. headers: {},
  191. connection: {
  192. remoteAddress: '192.170.18.1',
  193. },
  194. session: {
  195. user: this.user,
  196. },
  197. body: {
  198. projectName: this.projectName,
  199. },
  200. i18n: {
  201. translate() {},
  202. },
  203. ip: '192.170.18.1',
  204. }
  205. this.res = {
  206. locals: {
  207. jsPath: 'js path here',
  208. },
  209. setTimeout: sinon.stub(),
  210. }
  211. })
  212. describe('updateProjectSettings', function () {
  213. it('should update the name', function (done) {
  214. this.EditorController.renameProject = sinon.stub().callsArg(2)
  215. this.req.body = { name: (this.name = 'New name') }
  216. this.res.sendStatus = code => {
  217. this.EditorController.renameProject
  218. .calledWith(this.project_id, this.name)
  219. .should.equal(true)
  220. code.should.equal(204)
  221. done()
  222. }
  223. this.ProjectController.updateProjectSettings(this.req, this.res)
  224. })
  225. it('should update the compiler', function (done) {
  226. this.EditorController.setCompiler = sinon.stub().callsArg(2)
  227. this.req.body = { compiler: (this.compiler = 'pdflatex') }
  228. this.res.sendStatus = code => {
  229. this.EditorController.setCompiler
  230. .calledWith(this.project_id, this.compiler)
  231. .should.equal(true)
  232. code.should.equal(204)
  233. done()
  234. }
  235. this.ProjectController.updateProjectSettings(this.req, this.res)
  236. })
  237. it('should update the imageName', function (done) {
  238. this.EditorController.setImageName = sinon.stub().callsArg(2)
  239. this.req.body = { imageName: (this.imageName = 'texlive-1234.5') }
  240. this.res.sendStatus = code => {
  241. this.EditorController.setImageName
  242. .calledWith(this.project_id, this.imageName)
  243. .should.equal(true)
  244. code.should.equal(204)
  245. done()
  246. }
  247. this.ProjectController.updateProjectSettings(this.req, this.res)
  248. })
  249. it('should update the spell check language', function (done) {
  250. this.EditorController.setSpellCheckLanguage = sinon.stub().callsArg(2)
  251. this.req.body = { spellCheckLanguage: (this.languageCode = 'fr') }
  252. this.res.sendStatus = code => {
  253. this.EditorController.setSpellCheckLanguage
  254. .calledWith(this.project_id, this.languageCode)
  255. .should.equal(true)
  256. code.should.equal(204)
  257. done()
  258. }
  259. this.ProjectController.updateProjectSettings(this.req, this.res)
  260. })
  261. it('should update the root doc', function (done) {
  262. this.EditorController.setRootDoc = sinon.stub().callsArg(2)
  263. this.req.body = { rootDocId: (this.rootDocId = 'root-doc-id') }
  264. this.res.sendStatus = code => {
  265. this.EditorController.setRootDoc
  266. .calledWith(this.project_id, this.rootDocId)
  267. .should.equal(true)
  268. code.should.equal(204)
  269. done()
  270. }
  271. this.ProjectController.updateProjectSettings(this.req, this.res)
  272. })
  273. })
  274. describe('updateProjectAdminSettings', function () {
  275. it('should update the public access level', function (done) {
  276. this.EditorController.setPublicAccessLevel = sinon.stub().callsArg(2)
  277. this.req.body = {
  278. publicAccessLevel: (this.publicAccessLevel = 'readonly'),
  279. }
  280. this.res.sendStatus = code => {
  281. this.EditorController.setPublicAccessLevel
  282. .calledWith(this.project_id, this.publicAccessLevel)
  283. .should.equal(true)
  284. code.should.equal(204)
  285. done()
  286. }
  287. this.ProjectController.updateProjectAdminSettings(this.req, this.res)
  288. })
  289. })
  290. describe('deleteProject', function () {
  291. it('should call the project deleter', function (done) {
  292. this.res.sendStatus = code => {
  293. this.ProjectDeleter.deleteProject
  294. .calledWith(this.project_id, {
  295. deleterUser: this.user,
  296. ipAddress: this.req.ip,
  297. })
  298. .should.equal(true)
  299. code.should.equal(200)
  300. done()
  301. }
  302. this.ProjectController.deleteProject(this.req, this.res)
  303. })
  304. })
  305. describe('restoreProject', function () {
  306. it('should tell the project deleter', function (done) {
  307. this.res.sendStatus = code => {
  308. this.ProjectDeleter.restoreProject
  309. .calledWith(this.project_id)
  310. .should.equal(true)
  311. code.should.equal(200)
  312. done()
  313. }
  314. this.ProjectController.restoreProject(this.req, this.res)
  315. })
  316. })
  317. describe('cloneProject', function () {
  318. it('should call the project duplicator', function (done) {
  319. this.res.send = json => {
  320. this.ProjectDuplicator.duplicate
  321. .calledWith(this.user, this.project_id, this.projectName)
  322. .should.equal(true)
  323. json.project_id.should.equal(this.project_id)
  324. done()
  325. }
  326. this.ProjectController.cloneProject(this.req, this.res)
  327. })
  328. })
  329. describe('newProject', function () {
  330. it('should call the projectCreationHandler with createExampleProject', function (done) {
  331. this.req.body.template = 'example'
  332. this.res.send = json => {
  333. this.ProjectCreationHandler.createExampleProject
  334. .calledWith(this.user._id, this.projectName)
  335. .should.equal(true)
  336. this.ProjectCreationHandler.createBasicProject.called.should.equal(
  337. false
  338. )
  339. done()
  340. }
  341. this.ProjectController.newProject(this.req, this.res)
  342. })
  343. it('should call the projectCreationHandler with createBasicProject', function (done) {
  344. this.req.body.template = 'basic'
  345. this.res.send = json => {
  346. this.ProjectCreationHandler.createExampleProject.called.should.equal(
  347. false
  348. )
  349. this.ProjectCreationHandler.createBasicProject
  350. .calledWith(this.user._id, this.projectName)
  351. .should.equal(true)
  352. done()
  353. }
  354. this.ProjectController.newProject(this.req, this.res)
  355. })
  356. })
  357. describe('projectListPage', function () {
  358. beforeEach(function () {
  359. this.tags = [
  360. { name: 1, project_ids: ['1', '2', '3'] },
  361. { name: 2, project_ids: ['a', '1'] },
  362. { name: 3, project_ids: ['a', 'b', 'c', 'd'] },
  363. ]
  364. this.notifications = [
  365. {
  366. _id: '1',
  367. user_id: '2',
  368. templateKey: '3',
  369. messageOpts: '4',
  370. key: '5',
  371. },
  372. ]
  373. this.projects = [
  374. { _id: 1, lastUpdated: 1, owner_ref: 'user-1' },
  375. {
  376. _id: 2,
  377. lastUpdated: 2,
  378. owner_ref: 'user-2',
  379. lastUpdatedBy: 'user-1',
  380. },
  381. ]
  382. this.collabertions = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }]
  383. this.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }]
  384. this.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }]
  385. this.tokenReadOnly = [{ _id: 7, lastUpdated: 4, owner_ref: 'user-5' }]
  386. this.allProjects = {
  387. owned: this.projects,
  388. readAndWrite: this.collabertions,
  389. readOnly: this.readOnly,
  390. tokenReadAndWrite: this.tokenReadAndWrite,
  391. tokenReadOnly: this.tokenReadOnly,
  392. }
  393. this.users = {
  394. 'user-1': {
  395. first_name: 'James',
  396. },
  397. 'user-2': {
  398. first_name: 'Henry',
  399. },
  400. }
  401. this.users[this.user._id] = this.user // Owner
  402. this.UserModel.findById = (id, fields, callback) => {
  403. callback(null, this.users[id])
  404. }
  405. this.UserGetter.getUser = (id, fields, callback) => {
  406. callback(null, this.users[id])
  407. }
  408. this.LimitationsManager.hasPaidSubscription.callsArgWith(1, null, false)
  409. this.TagsHandler.getAllTags.callsArgWith(1, null, this.tags)
  410. this.NotificationsHandler.getUserNotifications = sinon
  411. .stub()
  412. .callsArgWith(1, null, this.notifications, {})
  413. this.ProjectGetter.findAllUsersProjects.callsArgWith(
  414. 2,
  415. null,
  416. this.allProjects
  417. )
  418. })
  419. it('should render the project/list page', function (done) {
  420. this.res.render = (pageName, opts) => {
  421. pageName.should.equal('project/list')
  422. done()
  423. }
  424. this.ProjectController.projectListPage(this.req, this.res)
  425. })
  426. it('should send the tags', function (done) {
  427. this.res.render = (pageName, opts) => {
  428. opts.tags.length.should.equal(this.tags.length)
  429. done()
  430. }
  431. this.ProjectController.projectListPage(this.req, this.res)
  432. })
  433. it('should create trigger ip matcher notifications', function (done) {
  434. this.settings.overleaf = true
  435. this.res.render = (pageName, opts) => {
  436. this.NotificationBuilder.ipMatcherAffiliation.called.should.equal(true)
  437. done()
  438. }
  439. this.ProjectController.projectListPage(this.req, this.res)
  440. })
  441. it('should send the projects', function (done) {
  442. this.res.render = (pageName, opts) => {
  443. opts.projects.length.should.equal(
  444. this.projects.length +
  445. this.collabertions.length +
  446. this.readOnly.length +
  447. this.tokenReadAndWrite.length +
  448. this.tokenReadOnly.length
  449. )
  450. done()
  451. }
  452. this.ProjectController.projectListPage(this.req, this.res)
  453. })
  454. it('should send the user', function (done) {
  455. this.res.render = (pageName, opts) => {
  456. opts.user.should.deep.equal(this.user)
  457. done()
  458. }
  459. this.ProjectController.projectListPage(this.req, this.res)
  460. })
  461. it('should inject the users', function (done) {
  462. this.res.render = (pageName, opts) => {
  463. opts.projects[0].owner.should.equal(
  464. this.users[this.projects[0].owner_ref]
  465. )
  466. opts.projects[1].owner.should.equal(
  467. this.users[this.projects[1].owner_ref]
  468. )
  469. opts.projects[1].lastUpdatedBy.should.equal(
  470. this.users[this.projects[1].lastUpdatedBy]
  471. )
  472. done()
  473. }
  474. this.ProjectController.projectListPage(this.req, this.res)
  475. })
  476. it('should send hasSubscription == false when no subscription', function (done) {
  477. this.res.render = (pageName, opts) => {
  478. opts.hasSubscription.should.equal(false)
  479. done()
  480. }
  481. this.ProjectController.projectListPage(this.req, this.res)
  482. })
  483. it('should send hasSubscription == true when there is a subscription', function (done) {
  484. this.LimitationsManager.hasPaidSubscription = sinon
  485. .stub()
  486. .callsArgWith(1, null, true)
  487. this.res.render = (pageName, opts) => {
  488. opts.hasSubscription.should.equal(true)
  489. done()
  490. }
  491. this.ProjectController.projectListPage(this.req, this.res)
  492. })
  493. describe('front widget', function (done) {
  494. beforeEach(function () {
  495. this.settings.overleaf = {
  496. front_chat_widget_room_id: 'chat-room-id',
  497. }
  498. })
  499. it('should show for paid users', function (done) {
  500. this.user.features.github = true
  501. this.user.features.dropbox = true
  502. this.res.render = (pageName, opts) => {
  503. opts.frontChatWidgetRoomId.should.equal(
  504. this.settings.overleaf.front_chat_widget_room_id
  505. )
  506. done()
  507. }
  508. this.ProjectController.projectListPage(this.req, this.res)
  509. })
  510. it('should show for sample users', function (done) {
  511. this.user._id = ObjectId('588f3ddae8ebc1bac07c9f00') // last two digits
  512. this.res.render = (pageName, opts) => {
  513. opts.frontChatWidgetRoomId.should.equal(
  514. this.settings.overleaf.front_chat_widget_room_id
  515. )
  516. done()
  517. }
  518. this.ProjectController.projectListPage(this.req, this.res)
  519. })
  520. it('should not show for non sample users', function (done) {
  521. this.user._id = ObjectId('588f3ddae8ebc1bac07c9fff') // last two digits
  522. this.res.render = (pageName, opts) => {
  523. expect(opts.frontChatWidgetRoomId).to.equal(undefined)
  524. done()
  525. }
  526. this.ProjectController.projectListPage(this.req, this.res)
  527. })
  528. })
  529. describe('With Institution SSO feature', function () {
  530. beforeEach(function (done) {
  531. this.institutionEmail = 'test@overleaf.com'
  532. this.institutionName = 'Overleaf'
  533. this.Features.hasFeature.withArgs('saml').returns(true)
  534. this.Features.hasFeature.withArgs('affiliations').returns(true)
  535. this.Features.hasFeature.withArgs('overleaf-integration').returns(true)
  536. done()
  537. })
  538. it('should show institution SSO available notification for confirmed domains', function () {
  539. this.UserGetter.getUserFullEmails.yields(null, [
  540. {
  541. email: 'test@overleaf.com',
  542. affiliation: {
  543. institution: {
  544. id: 1,
  545. confirmed: true,
  546. name: 'Overleaf',
  547. ssoBeta: false,
  548. ssoEnabled: true,
  549. },
  550. },
  551. },
  552. ])
  553. this.res.render = (pageName, opts) => {
  554. expect(opts.notificationsInstitution).to.deep.include({
  555. email: this.institutionEmail,
  556. institutionId: 1,
  557. institutionName: this.institutionName,
  558. templateKey: 'notification_institution_sso_available',
  559. })
  560. }
  561. this.ProjectController.projectListPage(this.req, this.res)
  562. })
  563. it('should show a linked notification', function () {
  564. this.req.session.saml = {
  565. institutionEmail: this.institutionEmail,
  566. linked: {
  567. hasEntitlement: false,
  568. universityName: this.institutionName,
  569. },
  570. }
  571. this.res.render = (pageName, opts) => {
  572. expect(opts.notificationsInstitution).to.deep.include({
  573. email: this.institutionEmail,
  574. institutionName: this.institutionName,
  575. templateKey: 'notification_institution_sso_linked',
  576. })
  577. }
  578. this.ProjectController.projectListPage(this.req, this.res)
  579. })
  580. it('should show a linked another email notification', function () {
  581. // when they request to link an email but the institution returns
  582. // a different email
  583. this.res.render = (pageName, opts) => {
  584. expect(opts.notificationsInstitution).to.deep.include({
  585. institutionEmail: this.institutionEmail,
  586. requestedEmail: 'requested@overleaf.com',
  587. templateKey: 'notification_institution_sso_non_canonical',
  588. })
  589. }
  590. this.req.session.saml = {
  591. emailNonCanonical: this.institutionEmail,
  592. institutionEmail: this.institutionEmail,
  593. requestedEmail: 'requested@overleaf.com',
  594. linked: {
  595. hasEntitlement: false,
  596. universityName: this.institutionName,
  597. },
  598. }
  599. this.ProjectController.projectListPage(this.req, this.res)
  600. })
  601. it('should show a notification when intent was to register via SSO but account existed', function () {
  602. this.res.render = (pageName, opts) => {
  603. expect(opts.notificationsInstitution).to.deep.include({
  604. email: this.institutionEmail,
  605. templateKey: 'notification_institution_sso_already_registered',
  606. })
  607. }
  608. this.req.session.saml = {
  609. institutionEmail: this.institutionEmail,
  610. linked: {
  611. hasEntitlement: false,
  612. universityName: 'Overleaf',
  613. },
  614. registerIntercept: {
  615. id: 1,
  616. name: 'Example University',
  617. },
  618. }
  619. this.ProjectController.projectListPage(this.req, this.res)
  620. })
  621. it('should not show a register notification if the flow was abandoned', function () {
  622. // could initially start to register with an SSO email and then
  623. // abandon flow and login with an existing non-institution SSO email
  624. this.res.render = (pageName, opts) => {
  625. expect(opts.notificationsInstitution).to.deep.not.include({
  626. email: 'test@overleaf.com',
  627. templateKey: 'notification_institution_sso_already_registered',
  628. })
  629. }
  630. this.req.session.saml = {
  631. registerIntercept: {
  632. id: 1,
  633. name: 'Example University',
  634. },
  635. }
  636. this.ProjectController.projectListPage(this.req, this.res)
  637. })
  638. it('should show error notification', function () {
  639. this.res.render = (pageName, opts) => {
  640. expect(opts.notificationsInstitution.length).to.equal(1)
  641. expect(opts.notificationsInstitution[0].templateKey).to.equal(
  642. 'notification_institution_sso_error'
  643. )
  644. expect(opts.notificationsInstitution[0].error).to.be.instanceof(
  645. Errors.SAMLAlreadyLinkedError
  646. )
  647. }
  648. this.req.session.saml = {
  649. institutionEmail: this.institutionEmail,
  650. error: new Errors.SAMLAlreadyLinkedError(),
  651. }
  652. this.ProjectController.projectListPage(this.req, this.res)
  653. })
  654. describe('for an unconfirmed domain for an SSO institution', function () {
  655. beforeEach(function (done) {
  656. this.UserGetter.getUserFullEmails.yields(null, [
  657. {
  658. email: 'test@overleaf-uncofirmed.com',
  659. affiliation: {
  660. institution: {
  661. id: 1,
  662. confirmed: false,
  663. name: 'Overleaf',
  664. ssoBeta: false,
  665. ssoEnabled: true,
  666. },
  667. },
  668. },
  669. ])
  670. done()
  671. })
  672. it('should not show institution SSO available notification', function () {
  673. this.res.render = (pageName, opts) => {
  674. expect(opts.notificationsInstitution.length).to.equal(0)
  675. }
  676. this.ProjectController.projectListPage(this.req, this.res)
  677. })
  678. })
  679. describe('when linking/logging in initiated on institution side', function () {
  680. it('should not show a linked another email notification', function () {
  681. // this is only used when initated on Overleaf,
  682. // because we keep track of the requested email they tried to link
  683. this.res.render = (pageName, opts) => {
  684. expect(opts.notificationsInstitution).to.not.deep.include({
  685. institutionEmail: this.institutionEmail,
  686. requestedEmail: undefined,
  687. templateKey: 'notification_institution_sso_non_canonical',
  688. })
  689. }
  690. this.req.session.saml = {
  691. emailNonCanonical: this.institutionEmail,
  692. institutionEmail: this.institutionEmail,
  693. linked: {
  694. hasEntitlement: false,
  695. universityName: this.institutionName,
  696. },
  697. }
  698. this.ProjectController.projectListPage(this.req, this.res)
  699. })
  700. })
  701. describe('Institution with SSO beta testable', function () {
  702. beforeEach(function (done) {
  703. this.UserGetter.getUserFullEmails.yields(null, [
  704. {
  705. email: 'beta@beta.com',
  706. affiliation: {
  707. institution: {
  708. id: 2,
  709. confirmed: true,
  710. name: 'Beta University',
  711. ssoBeta: true,
  712. ssoEnabled: false,
  713. },
  714. },
  715. },
  716. ])
  717. done()
  718. })
  719. it('should show institution SSO available notification when on a beta testing session', function () {
  720. this.req.session.samlBeta = true
  721. this.res.render = (pageName, opts) => {
  722. expect(opts.notificationsInstitution).to.deep.include({
  723. email: 'beta@beta.com',
  724. institutionId: 2,
  725. institutionName: 'Beta University',
  726. templateKey: 'notification_institution_sso_available',
  727. })
  728. }
  729. this.ProjectController.projectListPage(this.req, this.res)
  730. })
  731. it('should not show institution SSO available notification when not on a beta testing session', function () {
  732. this.req.session.samlBeta = false
  733. this.res.render = (pageName, opts) => {
  734. expect(opts.notificationsInstitution).to.deep.not.include({
  735. email: 'test@overleaf.com',
  736. institutionId: 1,
  737. institutionName: 'Overleaf',
  738. templateKey: 'notification_institution_sso_available',
  739. })
  740. }
  741. this.ProjectController.projectListPage(this.req, this.res)
  742. })
  743. })
  744. })
  745. describe('Without Institution SSO feature', function () {
  746. beforeEach(function (done) {
  747. this.Features.hasFeature.withArgs('saml').returns(false)
  748. done()
  749. })
  750. it('should not show institution sso available notification', function () {
  751. this.res.render = (pageName, opts) => {
  752. expect(opts.notificationsInstitution).to.deep.not.include({
  753. email: 'test@overleaf.com',
  754. institutionId: 1,
  755. institutionName: 'Overleaf',
  756. templateKey: 'notification_institution_sso_available',
  757. })
  758. }
  759. this.ProjectController.projectListPage(this.req, this.res)
  760. })
  761. })
  762. })
  763. describe('projectListPage with duplicate projects', function () {
  764. beforeEach(function () {
  765. this.tags = [
  766. { name: 1, project_ids: ['1', '2', '3'] },
  767. { name: 2, project_ids: ['a', '1'] },
  768. { name: 3, project_ids: ['a', 'b', 'c', 'd'] },
  769. ]
  770. this.notifications = [
  771. {
  772. _id: '1',
  773. user_id: '2',
  774. templateKey: '3',
  775. messageOpts: '4',
  776. key: '5',
  777. },
  778. ]
  779. this.projects = [
  780. { _id: 1, lastUpdated: 1, owner_ref: 'user-1' },
  781. { _id: 2, lastUpdated: 2, owner_ref: 'user-2' },
  782. ]
  783. this.collabertions = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }]
  784. this.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }]
  785. this.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }]
  786. this.tokenReadOnly = [
  787. { _id: 6, lastUpdated: 5, owner_ref: 'user-4' }, // Also in tokenReadAndWrite
  788. { _id: 7, lastUpdated: 4, owner_ref: 'user-5' },
  789. ]
  790. this.allProjects = {
  791. owned: this.projects,
  792. readAndWrite: this.collabertions,
  793. readOnly: this.readOnly,
  794. tokenReadAndWrite: this.tokenReadAndWrite,
  795. tokenReadOnly: this.tokenReadOnly,
  796. }
  797. this.users = {
  798. 'user-1': {
  799. first_name: 'James',
  800. },
  801. 'user-2': {
  802. first_name: 'Henry',
  803. },
  804. }
  805. this.users[this.user._id] = this.user // Owner
  806. this.UserModel.findById = (id, fields, callback) => {
  807. callback(null, this.users[id])
  808. }
  809. this.LimitationsManager.hasPaidSubscription.callsArgWith(1, null, false)
  810. this.TagsHandler.getAllTags.callsArgWith(1, null, this.tags)
  811. this.NotificationsHandler.getUserNotifications = sinon
  812. .stub()
  813. .callsArgWith(1, null, this.notifications, {})
  814. this.ProjectGetter.findAllUsersProjects.callsArgWith(
  815. 2,
  816. null,
  817. this.allProjects
  818. )
  819. })
  820. it('should render the project/list page', function (done) {
  821. this.res.render = (pageName, opts) => {
  822. pageName.should.equal('project/list')
  823. done()
  824. }
  825. this.ProjectController.projectListPage(this.req, this.res)
  826. })
  827. it('should omit one of the projects', function (done) {
  828. this.res.render = (pageName, opts) => {
  829. opts.projects.length.should.equal(
  830. this.projects.length +
  831. this.collabertions.length +
  832. this.readOnly.length +
  833. this.tokenReadAndWrite.length +
  834. this.tokenReadOnly.length -
  835. 1
  836. )
  837. done()
  838. }
  839. this.ProjectController.projectListPage(this.req, this.res)
  840. })
  841. })
  842. describe('renameProject', function () {
  843. beforeEach(function () {
  844. this.newProjectName = 'my supper great new project'
  845. this.req.body.newProjectName = this.newProjectName
  846. })
  847. it('should call the editor controller', function (done) {
  848. this.EditorController.renameProject.callsArgWith(2)
  849. this.res.sendStatus = code => {
  850. code.should.equal(200)
  851. this.EditorController.renameProject
  852. .calledWith(this.project_id, this.newProjectName)
  853. .should.equal(true)
  854. done()
  855. }
  856. this.ProjectController.renameProject(this.req, this.res)
  857. })
  858. it('should send an error to next() if there is a problem', function (done) {
  859. let error
  860. this.EditorController.renameProject.callsArgWith(
  861. 2,
  862. (error = new Error('problem'))
  863. )
  864. const next = e => {
  865. e.should.equal(error)
  866. done()
  867. }
  868. this.ProjectController.renameProject(this.req, this.res, next)
  869. })
  870. })
  871. describe('loadEditor', function () {
  872. beforeEach(function () {
  873. this.settings.editorIsOpen = true
  874. this.project = {
  875. name: 'my proj',
  876. _id: '213123kjlkj',
  877. owner_ref: '59fc84d5fbea77482d436e1b',
  878. }
  879. this.brandedProject = {
  880. name: 'my branded proj',
  881. _id: '3252332',
  882. owner_ref: '59fc84d5fbea77482d436e1b',
  883. brandVariationId: '12',
  884. }
  885. this.user = {
  886. _id: '588f3ddae8ebc1bac07c9fa4',
  887. ace: {
  888. fontSize: 'massive',
  889. theme: 'sexy',
  890. },
  891. email: 'bob@bob.com',
  892. refProviders: {
  893. mendeley: { encrypted: 'aaaa' },
  894. zotero: { encrypted: 'bbbb' },
  895. },
  896. }
  897. this.ProjectGetter.getProject.callsArgWith(2, null, this.project)
  898. this.UserModel.findById.callsArgWith(2, null, this.user)
  899. this.SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {})
  900. this.AuthorizationManager.getPrivilegeLevelForProject.callsArgWith(
  901. 3,
  902. null,
  903. 'owner'
  904. )
  905. this.ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub()
  906. this.InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1)
  907. this.ProjectUpdateHandler.markAsOpened.callsArgWith(1)
  908. })
  909. it('should render the project/editor page', function (done) {
  910. this.res.render = (pageName, opts) => {
  911. pageName.should.equal('project/editor')
  912. done()
  913. }
  914. this.ProjectController.loadEditor(this.req, this.res)
  915. })
  916. it('should add user', function (done) {
  917. this.res.render = (pageName, opts) => {
  918. opts.user.email.should.equal(this.user.email)
  919. done()
  920. }
  921. this.ProjectController.loadEditor(this.req, this.res)
  922. })
  923. it('should sanitize refProviders', function (done) {
  924. this.res.render = (_pageName, opts) => {
  925. expect(opts.user.refProviders).to.deep.equal({
  926. mendeley: true,
  927. zotero: true,
  928. })
  929. done()
  930. }
  931. this.ProjectController.loadEditor(this.req, this.res)
  932. })
  933. it('should add on userSettings', function (done) {
  934. this.res.render = (pageName, opts) => {
  935. opts.userSettings.fontSize.should.equal(this.user.ace.fontSize)
  936. opts.userSettings.editorTheme.should.equal(this.user.ace.theme)
  937. done()
  938. }
  939. this.ProjectController.loadEditor(this.req, this.res)
  940. })
  941. it('should add isRestrictedTokenMember', function (done) {
  942. this.res.render = (pageName, opts) => {
  943. opts.isRestrictedTokenMember.should.exist
  944. opts.isRestrictedTokenMember.should.equal(false)
  945. return done()
  946. }
  947. return this.ProjectController.loadEditor(this.req, this.res)
  948. })
  949. it('should set isRestrictedTokenMember when appropriate', function (done) {
  950. this.AuthorizationManager.isRestrictedUser.returns(true)
  951. this.res.render = (pageName, opts) => {
  952. opts.isRestrictedTokenMember.should.exist
  953. opts.isRestrictedTokenMember.should.equal(true)
  954. return done()
  955. }
  956. return this.ProjectController.loadEditor(this.req, this.res)
  957. })
  958. it('should render the closed page if the editor is closed', function (done) {
  959. this.settings.editorIsOpen = false
  960. this.res.render = (pageName, opts) => {
  961. pageName.should.equal('general/closed')
  962. done()
  963. }
  964. this.ProjectController.loadEditor(this.req, this.res)
  965. })
  966. it('should not render the page if the project can not be accessed', function (done) {
  967. this.AuthorizationManager.getPrivilegeLevelForProject = sinon
  968. .stub()
  969. .callsArgWith(3, null, null)
  970. this.res.sendStatus = (resCode, opts) => {
  971. resCode.should.equal(401)
  972. done()
  973. }
  974. this.ProjectController.loadEditor(this.req, this.res)
  975. })
  976. it('should reactivateProjectIfRequired', function (done) {
  977. this.res.render = (pageName, opts) => {
  978. this.InactiveProjectManager.reactivateProjectIfRequired
  979. .calledWith(this.project_id)
  980. .should.equal(true)
  981. done()
  982. }
  983. this.ProjectController.loadEditor(this.req, this.res)
  984. })
  985. it('should mark project as opened', function (done) {
  986. this.res.render = (pageName, opts) => {
  987. this.ProjectUpdateHandler.markAsOpened
  988. .calledWith(this.project_id)
  989. .should.equal(true)
  990. done()
  991. }
  992. this.ProjectController.loadEditor(this.req, this.res)
  993. })
  994. it('should call the brand variations handler for branded projects', function (done) {
  995. this.ProjectGetter.getProject.callsArgWith(2, null, this.brandedProject)
  996. this.res.render = (pageName, opts) => {
  997. this.BrandVariationsHandler.getBrandVariationById
  998. .calledWith()
  999. .should.equal(true)
  1000. done()
  1001. }
  1002. this.ProjectController.loadEditor(this.req, this.res)
  1003. })
  1004. it('should not call the brand variations handler for unbranded projects', function (done) {
  1005. this.res.render = (pageName, opts) => {
  1006. this.BrandVariationsHandler.getBrandVariationById.called.should.equal(
  1007. false
  1008. )
  1009. done()
  1010. }
  1011. this.ProjectController.loadEditor(this.req, this.res)
  1012. })
  1013. it('should expose the brand variation details as locals for branded projects', function (done) {
  1014. this.ProjectGetter.getProject.callsArgWith(2, null, this.brandedProject)
  1015. this.res.render = (pageName, opts) => {
  1016. opts.brandVariation.should.deep.equal(this.brandVariationDetails)
  1017. done()
  1018. }
  1019. this.ProjectController.loadEditor(this.req, this.res)
  1020. })
  1021. it('flushes the project to TPDS if a flush is pending', function (done) {
  1022. this.res.render = () => {
  1023. this.TpdsProjectFlusher.flushProjectToTpdsIfNeeded.should.have.been.calledWith(
  1024. this.project_id
  1025. )
  1026. done()
  1027. }
  1028. this.ProjectController.loadEditor(this.req, this.res)
  1029. })
  1030. it('should refresh the user features if the epoch is outdated', function (done) {
  1031. this.FeaturesUpdater.featuresEpochIsCurrent = sinon.stub().returns(false)
  1032. this.res.render = () => {
  1033. this.FeaturesUpdater.refreshFeatures.should.have.been.calledWith(
  1034. this.user._id,
  1035. 'load-editor'
  1036. )
  1037. done()
  1038. }
  1039. this.ProjectController.loadEditor(this.req, this.res)
  1040. })
  1041. describe('pdf caching feature flags', function () {
  1042. /* eslint-disable mocha/no-identical-title */
  1043. function showNoVariant() {
  1044. beforeEach(function () {
  1045. this.SplitTestHandler.getTestSegmentation = sinon
  1046. .stub()
  1047. .yields(null, { enabled: false })
  1048. })
  1049. }
  1050. function showVariant(variant) {
  1051. beforeEach(function () {
  1052. this.SplitTestHandler.getTestSegmentation = sinon
  1053. .stub()
  1054. .yields(null, { enabled: true, variant })
  1055. })
  1056. }
  1057. function expectBandwidthTrackingEnabled() {
  1058. it('should track pdf bandwidth', function (done) {
  1059. this.res.render = (pageName, opts) => {
  1060. expect(opts.trackPdfDownload).to.equal(true)
  1061. done()
  1062. }
  1063. this.ProjectController.loadEditor(this.req, this.res)
  1064. })
  1065. }
  1066. function expectPDFCachingEnabled() {
  1067. it('should enable pdf caching', function (done) {
  1068. this.res.render = (pageName, opts) => {
  1069. expect(opts.enablePdfCaching).to.equal(true)
  1070. done()
  1071. }
  1072. this.ProjectController.loadEditor(this.req, this.res)
  1073. })
  1074. }
  1075. function expectBandwidthTrackingDisabled() {
  1076. it('should not track pdf bandwidth', function (done) {
  1077. this.res.render = (pageName, opts) => {
  1078. expect(opts.trackPdfDownload).to.equal(false)
  1079. done()
  1080. }
  1081. this.ProjectController.loadEditor(this.req, this.res)
  1082. })
  1083. }
  1084. function expectPDFCachingDisabled() {
  1085. it('should disable pdf caching', function (done) {
  1086. this.res.render = (pageName, opts) => {
  1087. expect(opts.enablePdfCaching).to.equal(false)
  1088. done()
  1089. }
  1090. this.ProjectController.loadEditor(this.req, this.res)
  1091. })
  1092. }
  1093. function expectToCollectMetricsAndCachePDF() {
  1094. describe('with no query', function () {
  1095. expectBandwidthTrackingEnabled()
  1096. expectPDFCachingEnabled()
  1097. })
  1098. describe('with enable_pdf_caching=false', function () {
  1099. beforeEach(function () {
  1100. this.req.query.enable_pdf_caching = 'false'
  1101. })
  1102. expectBandwidthTrackingDisabled()
  1103. expectPDFCachingDisabled()
  1104. })
  1105. describe('with enable_pdf_caching=true', function () {
  1106. beforeEach(function () {
  1107. this.req.query.enable_pdf_caching = 'true'
  1108. })
  1109. expectBandwidthTrackingEnabled()
  1110. expectPDFCachingEnabled()
  1111. })
  1112. }
  1113. function expectToCollectMetricsOnly() {
  1114. describe('with no query', function () {
  1115. expectBandwidthTrackingEnabled()
  1116. expectPDFCachingDisabled()
  1117. })
  1118. describe('with enable_pdf_caching=false', function () {
  1119. beforeEach(function () {
  1120. this.req.query.enable_pdf_caching = 'false'
  1121. })
  1122. expectBandwidthTrackingDisabled()
  1123. expectPDFCachingDisabled()
  1124. })
  1125. describe('with enable_pdf_caching=true', function () {
  1126. beforeEach(function () {
  1127. this.req.query.enable_pdf_caching = 'true'
  1128. })
  1129. expectBandwidthTrackingEnabled()
  1130. expectPDFCachingDisabled()
  1131. })
  1132. }
  1133. function expectToCachePDFOnly() {
  1134. describe('with no query', function () {
  1135. expectBandwidthTrackingDisabled()
  1136. expectPDFCachingEnabled()
  1137. })
  1138. describe('with enable_pdf_caching=false', function () {
  1139. beforeEach(function () {
  1140. this.req.query.enable_pdf_caching = 'false'
  1141. })
  1142. expectBandwidthTrackingDisabled()
  1143. expectPDFCachingDisabled()
  1144. })
  1145. describe('with enable_pdf_caching=true', function () {
  1146. beforeEach(function () {
  1147. this.req.query.enable_pdf_caching = 'true'
  1148. })
  1149. expectBandwidthTrackingDisabled()
  1150. expectPDFCachingEnabled()
  1151. })
  1152. }
  1153. function expectToNotBeEnrolledAtAll() {
  1154. describe('with no query', function () {
  1155. expectBandwidthTrackingDisabled()
  1156. expectPDFCachingDisabled()
  1157. })
  1158. describe('with enable_pdf_caching=false', function () {
  1159. beforeEach(function () {
  1160. this.req.query.enable_pdf_caching = 'false'
  1161. })
  1162. expectBandwidthTrackingDisabled()
  1163. expectPDFCachingDisabled()
  1164. })
  1165. describe('with enable_pdf_caching=true', function () {
  1166. beforeEach(function () {
  1167. this.req.query.enable_pdf_caching = 'true'
  1168. })
  1169. expectBandwidthTrackingDisabled()
  1170. expectPDFCachingDisabled()
  1171. })
  1172. }
  1173. function tagAnonymous() {
  1174. beforeEach(function () {
  1175. this.SessionManager.isUserLoggedIn = sinon.stub().returns(false)
  1176. })
  1177. }
  1178. beforeEach(function () {
  1179. this.settings.enablePdfCaching = true
  1180. })
  1181. describe('during regular roll-out', function () {
  1182. before(function () {
  1183. this.skip()
  1184. })
  1185. describe('disabled', function () {
  1186. showNoVariant()
  1187. describe('regular user', function () {
  1188. expectToNotBeEnrolledAtAll()
  1189. })
  1190. describe('anonymous user', function () {
  1191. tagAnonymous()
  1192. expectToCachePDFOnly()
  1193. })
  1194. })
  1195. describe('variant=collect-metrics', function () {
  1196. showVariant('collect-metrics')
  1197. describe('regular user', function () {
  1198. expectToCollectMetricsOnly()
  1199. })
  1200. describe('anonymous user', function () {
  1201. tagAnonymous()
  1202. expectToCachePDFOnly()
  1203. })
  1204. })
  1205. describe('variant=collect-metrics-and-enable-caching', function () {
  1206. showVariant('collect-metrics-and-enable-caching')
  1207. describe('regular user', function () {
  1208. expectToCollectMetricsAndCachePDF()
  1209. })
  1210. describe('anonymous user', function () {
  1211. tagAnonymous()
  1212. expectToCachePDFOnly()
  1213. })
  1214. })
  1215. describe('variant=enable-caching-only', function () {
  1216. showVariant('enable-caching-only')
  1217. describe('regular user', function () {
  1218. expectToCachePDFOnly()
  1219. })
  1220. describe('anonymous user', function () {
  1221. tagAnonymous()
  1222. expectToCachePDFOnly()
  1223. })
  1224. })
  1225. })
  1226. describe('during opt-in only', function () {
  1227. describe('with no query', function () {
  1228. expectBandwidthTrackingDisabled()
  1229. expectPDFCachingDisabled()
  1230. })
  1231. describe('with enable_pdf_caching=false', function () {
  1232. beforeEach(function () {
  1233. this.req.query.enable_pdf_caching = 'false'
  1234. })
  1235. expectBandwidthTrackingDisabled()
  1236. expectPDFCachingDisabled()
  1237. })
  1238. describe('with enable_pdf_caching=true', function () {
  1239. beforeEach(function () {
  1240. this.req.query.enable_pdf_caching = 'true'
  1241. })
  1242. expectBandwidthTrackingEnabled()
  1243. expectPDFCachingEnabled()
  1244. })
  1245. })
  1246. })
  1247. describe('wsUrl', function () {
  1248. function checkLoadEditorWsMetric(metric) {
  1249. it(`should inc metric ${metric}`, function (done) {
  1250. this.res.render = () => {
  1251. this.Metrics.inc.calledWith(metric).should.equal(true)
  1252. done()
  1253. }
  1254. this.ProjectController.loadEditor(this.req, this.res)
  1255. })
  1256. }
  1257. function checkWsFallback(isBeta, isV2) {
  1258. describe('with ws=fallback', function () {
  1259. beforeEach(function () {
  1260. this.req.query = {}
  1261. this.req.query.ws = 'fallback'
  1262. })
  1263. it('should unset the wsUrl', function (done) {
  1264. this.res.render = (pageName, opts) => {
  1265. ;(opts.wsUrl || '/socket.io').should.equal('/socket.io')
  1266. done()
  1267. }
  1268. this.ProjectController.loadEditor(this.req, this.res)
  1269. })
  1270. checkLoadEditorWsMetric(
  1271. `load-editor-ws${isBeta ? '-beta' : ''}${
  1272. isV2 ? '-v2' : ''
  1273. }-fallback`
  1274. )
  1275. })
  1276. }
  1277. beforeEach(function () {
  1278. this.settings.wsUrl = '/other.socket.io'
  1279. })
  1280. it('should set the custom wsUrl', function (done) {
  1281. this.res.render = (pageName, opts) => {
  1282. opts.wsUrl.should.equal('/other.socket.io')
  1283. done()
  1284. }
  1285. this.ProjectController.loadEditor(this.req, this.res)
  1286. })
  1287. checkLoadEditorWsMetric('load-editor-ws')
  1288. checkWsFallback(false)
  1289. describe('beta program', function () {
  1290. beforeEach(function () {
  1291. this.settings.wsUrlBeta = '/beta.socket.io'
  1292. })
  1293. describe('for a normal user', function () {
  1294. it('should set the normal custom wsUrl', function (done) {
  1295. this.res.render = (pageName, opts) => {
  1296. opts.wsUrl.should.equal('/other.socket.io')
  1297. done()
  1298. }
  1299. this.ProjectController.loadEditor(this.req, this.res)
  1300. })
  1301. checkLoadEditorWsMetric('load-editor-ws')
  1302. checkWsFallback(false)
  1303. })
  1304. describe('for a beta user', function () {
  1305. beforeEach(function () {
  1306. this.user.betaProgram = true
  1307. })
  1308. it('should set the beta wsUrl', function (done) {
  1309. this.res.render = (pageName, opts) => {
  1310. opts.wsUrl.should.equal('/beta.socket.io')
  1311. done()
  1312. }
  1313. this.ProjectController.loadEditor(this.req, this.res)
  1314. })
  1315. checkLoadEditorWsMetric('load-editor-ws-beta')
  1316. checkWsFallback(true)
  1317. })
  1318. })
  1319. describe('v2-rollout', function () {
  1320. beforeEach(function () {
  1321. this.settings.wsUrlBeta = '/beta.socket.io'
  1322. this.settings.wsUrlV2 = '/socket.io.v2'
  1323. })
  1324. function checkNonMatch() {
  1325. it('should set the normal custom wsUrl', function (done) {
  1326. this.res.render = (pageName, opts) => {
  1327. opts.wsUrl.should.equal('/other.socket.io')
  1328. done()
  1329. }
  1330. this.ProjectController.loadEditor(this.req, this.res)
  1331. })
  1332. checkLoadEditorWsMetric('load-editor-ws')
  1333. checkWsFallback(false)
  1334. }
  1335. function checkMatch() {
  1336. it('should set the v2 wsUrl', function (done) {
  1337. this.res.render = (pageName, opts) => {
  1338. opts.wsUrl.should.equal('/socket.io.v2')
  1339. done()
  1340. }
  1341. this.ProjectController.loadEditor(this.req, this.res)
  1342. })
  1343. checkLoadEditorWsMetric('load-editor-ws-v2')
  1344. checkWsFallback(false, true)
  1345. }
  1346. function checkForBetaUser() {
  1347. describe('for a beta user', function () {
  1348. beforeEach(function () {
  1349. this.user.betaProgram = true
  1350. })
  1351. it('should set the beta wsUrl', function (done) {
  1352. this.res.render = (pageName, opts) => {
  1353. opts.wsUrl.should.equal('/beta.socket.io')
  1354. done()
  1355. }
  1356. this.ProjectController.loadEditor(this.req, this.res)
  1357. })
  1358. checkLoadEditorWsMetric('load-editor-ws-beta')
  1359. checkWsFallback(true)
  1360. })
  1361. }
  1362. describe('when the roll out percentage is 0', function () {
  1363. beforeEach(function () {
  1364. this.settings.wsUrlV2Percentage = 0
  1365. })
  1366. describe('when the projectId does not match (0)', function () {
  1367. beforeEach(function () {
  1368. this.req.params.Project_id = ObjectId.createFromTime(0)
  1369. })
  1370. checkNonMatch()
  1371. })
  1372. describe('when the projectId does not match (42)', function () {
  1373. beforeEach(function () {
  1374. this.req.params.Project_id = ObjectId.createFromTime(42)
  1375. })
  1376. checkNonMatch()
  1377. })
  1378. checkForBetaUser()
  1379. })
  1380. describe('when the roll out percentage is 1', function () {
  1381. beforeEach(function () {
  1382. this.settings.wsUrlV2Percentage = 1
  1383. })
  1384. describe('when the projectId matches (0)', function () {
  1385. beforeEach(function () {
  1386. this.req.params.Project_id = ObjectId.createFromTime(0)
  1387. })
  1388. checkMatch()
  1389. checkForBetaUser()
  1390. })
  1391. describe('when the projectId does not match (1)', function () {
  1392. beforeEach(function () {
  1393. this.req.params.Project_id = ObjectId.createFromTime(1)
  1394. })
  1395. checkNonMatch()
  1396. checkForBetaUser()
  1397. })
  1398. describe('when the projectId does not match (42)', function () {
  1399. beforeEach(function () {
  1400. this.req.params.Project_id = ObjectId.createFromTime(42)
  1401. })
  1402. checkNonMatch()
  1403. })
  1404. })
  1405. describe('when the roll out percentage is 10', function () {
  1406. beforeEach(function () {
  1407. this.settings.wsUrlV2Percentage = 10
  1408. })
  1409. describe('when the projectId matches (0)', function () {
  1410. beforeEach(function () {
  1411. this.req.params.Project_id = ObjectId.createFromTime(0)
  1412. })
  1413. checkMatch()
  1414. })
  1415. describe('when the projectId matches (9)', function () {
  1416. beforeEach(function () {
  1417. this.req.params.Project_id = ObjectId.createFromTime(9)
  1418. })
  1419. checkMatch()
  1420. checkForBetaUser()
  1421. })
  1422. describe('when the projectId does not match (10)', function () {
  1423. beforeEach(function () {
  1424. this.req.params.Project_id = ObjectId.createFromTime(10)
  1425. })
  1426. checkNonMatch()
  1427. })
  1428. describe('when the projectId does not match (42)', function () {
  1429. beforeEach(function () {
  1430. this.req.params.Project_id = ObjectId.createFromTime(42)
  1431. })
  1432. checkNonMatch()
  1433. checkForBetaUser()
  1434. })
  1435. })
  1436. describe('when the roll out percentage is 100', function () {
  1437. beforeEach(function () {
  1438. this.settings.wsUrlV2Percentage = 100
  1439. })
  1440. describe('when the projectId matches (0)', function () {
  1441. beforeEach(function () {
  1442. this.req.params.Project_id = ObjectId.createFromTime(0)
  1443. })
  1444. checkMatch()
  1445. checkForBetaUser()
  1446. })
  1447. describe('when the projectId matches (10)', function () {
  1448. beforeEach(function () {
  1449. this.req.params.Project_id = ObjectId.createFromTime(10)
  1450. })
  1451. checkMatch()
  1452. })
  1453. describe('when the projectId matches (42)', function () {
  1454. beforeEach(function () {
  1455. this.req.params.Project_id = ObjectId.createFromTime(42)
  1456. })
  1457. checkMatch()
  1458. })
  1459. describe('when the projectId matches (99)', function () {
  1460. beforeEach(function () {
  1461. this.req.params.Project_id = ObjectId.createFromTime(99)
  1462. })
  1463. checkMatch()
  1464. })
  1465. })
  1466. })
  1467. })
  1468. describe('feature flags', function () {
  1469. describe('showNewPdfPreview', function () {
  1470. it('should be false by default', function (done) {
  1471. this.res.render = (pageName, opts) => {
  1472. expect(opts.showNewPdfPreview).to.be.false
  1473. done()
  1474. }
  1475. this.ProjectController.loadEditor(this.req, this.res)
  1476. })
  1477. it('should be true when ?new_pdf_preview=true ', function (done) {
  1478. this.res.render = (pageName, opts) => {
  1479. expect(opts.showNewPdfPreview).to.be.true
  1480. done()
  1481. }
  1482. this.req.query.new_pdf_preview = 'true'
  1483. this.ProjectController.loadEditor(this.req, this.res)
  1484. })
  1485. it('should be true for alpha group', function (done) {
  1486. this.res.render = (pageName, opts) => {
  1487. expect(opts.showNewPdfPreview).to.be.true
  1488. done()
  1489. }
  1490. this.user.alphaProgram = true
  1491. this.ProjectController.loadEditor(this.req, this.res)
  1492. })
  1493. it('should be false when when ?new_pdf_preview=true and alpha group', function (done) {
  1494. this.res.render = (pageName, opts) => {
  1495. expect(opts.showNewPdfPreview).to.be.true
  1496. done()
  1497. }
  1498. this.user.alphaProgram = true
  1499. this.req.query.new_pdf_preview = 'false'
  1500. this.ProjectController.loadEditor(this.req, this.res)
  1501. })
  1502. })
  1503. describe('showPdfDetach', function () {
  1504. describe('showPdfDetach=false', function () {
  1505. it('should be false by default', function (done) {
  1506. this.res.render = (pageName, opts) => {
  1507. expect(opts.showPdfDetach).to.be.false
  1508. done()
  1509. }
  1510. this.ProjectController.loadEditor(this.req, this.res)
  1511. })
  1512. it('should be false by default, even when ?new_pdf_preview=true', function (done) {
  1513. this.res.render = (pageName, opts) => {
  1514. expect(opts.showPdfDetach).to.be.false
  1515. expect(opts.showNewPdfPreview).to.be.true
  1516. done()
  1517. }
  1518. this.req.query.new_pdf_preview = 'true'
  1519. this.ProjectController.loadEditor(this.req, this.res)
  1520. })
  1521. it('should be false when when ?pdf_detach=true and alpha group', function (done) {
  1522. this.res.render = (pageName, opts) => {
  1523. done()
  1524. }
  1525. this.user.alphaProgram = true
  1526. this.req.query.pdf_detach = 'false'
  1527. this.ProjectController.loadEditor(this.req, this.res)
  1528. })
  1529. })
  1530. describe('showPdfDetach=true', function () {
  1531. it('should be true when ?pdf_detach=true, and set showNewPdfPreview as true ', function (done) {
  1532. this.res.render = (pageName, opts) => {
  1533. expect(opts.showPdfDetach).to.be.true
  1534. expect(opts.showNewPdfPreview).to.be.true
  1535. done()
  1536. }
  1537. this.req.query.pdf_detach = 'true'
  1538. this.ProjectController.loadEditor(this.req, this.res)
  1539. })
  1540. it('should be true for alpha group, and set showNewPdfPreview as true', function (done) {
  1541. this.res.render = (pageName, opts) => {
  1542. expect(opts.showPdfDetach).to.be.true
  1543. expect(opts.showNewPdfPreview).to.be.true
  1544. done()
  1545. }
  1546. this.user.alphaProgram = true
  1547. this.ProjectController.loadEditor(this.req, this.res)
  1548. })
  1549. })
  1550. })
  1551. })
  1552. })
  1553. describe('userProjectsJson', function () {
  1554. beforeEach(function (done) {
  1555. const projects = [
  1556. {
  1557. archived: true,
  1558. trashed: true,
  1559. id: 'a',
  1560. name: 'A',
  1561. accessLevel: 'a',
  1562. somethingElse: 1,
  1563. },
  1564. {
  1565. archived: false,
  1566. id: 'b',
  1567. name: 'B',
  1568. accessLevel: 'b',
  1569. somethingElse: 1,
  1570. },
  1571. {
  1572. archived: false,
  1573. trashed: true,
  1574. id: 'c',
  1575. name: 'C',
  1576. accessLevel: 'c',
  1577. somethingElse: 1,
  1578. },
  1579. {
  1580. archived: false,
  1581. trashed: false,
  1582. id: 'd',
  1583. name: 'D',
  1584. accessLevel: 'd',
  1585. somethingElse: 1,
  1586. },
  1587. ]
  1588. this.ProjectHelper.isArchivedOrTrashed
  1589. .withArgs(projects[0], this.user._id)
  1590. .returns(true)
  1591. this.ProjectHelper.isArchivedOrTrashed
  1592. .withArgs(projects[1], this.user._id)
  1593. .returns(false)
  1594. this.ProjectHelper.isArchivedOrTrashed
  1595. .withArgs(projects[2], this.user._id)
  1596. .returns(true)
  1597. this.ProjectHelper.isArchivedOrTrashed
  1598. .withArgs(projects[3], this.user._id)
  1599. .returns(false)
  1600. this.ProjectGetter.findAllUsersProjects = sinon
  1601. .stub()
  1602. .callsArgWith(2, null, [])
  1603. this.ProjectController._buildProjectList = sinon.stub().returns(projects)
  1604. this.SessionManager.getLoggedInUserId = sinon
  1605. .stub()
  1606. .returns(this.user._id)
  1607. done()
  1608. })
  1609. it('should produce a list of projects', function (done) {
  1610. this.res.json = data => {
  1611. expect(data).to.deep.equal({
  1612. projects: [
  1613. { _id: 'b', name: 'B', accessLevel: 'b' },
  1614. { _id: 'd', name: 'D', accessLevel: 'd' },
  1615. ],
  1616. })
  1617. done()
  1618. }
  1619. this.ProjectController.userProjectsJson(this.req, this.res, this.next)
  1620. })
  1621. })
  1622. describe('projectEntitiesJson', function () {
  1623. beforeEach(function () {
  1624. this.SessionManager.getLoggedInUserId = sinon.stub().returns('abc')
  1625. this.req.params = { Project_id: 'abcd' }
  1626. this.project = { _id: 'abcd' }
  1627. this.docs = [
  1628. { path: '/things/b.txt', doc: true },
  1629. { path: '/main.tex', doc: true },
  1630. ]
  1631. this.files = [{ path: '/things/a.txt' }]
  1632. this.ProjectGetter.getProject = sinon
  1633. .stub()
  1634. .callsArgWith(1, null, this.project)
  1635. this.ProjectEntityHandler.getAllEntitiesFromProject = sinon
  1636. .stub()
  1637. .callsArgWith(1, null, this.docs, this.files)
  1638. })
  1639. it('should produce a list of entities', function (done) {
  1640. this.res.json = data => {
  1641. expect(data).to.deep.equal({
  1642. project_id: 'abcd',
  1643. entities: [
  1644. { path: '/main.tex', type: 'doc' },
  1645. { path: '/things/a.txt', type: 'file' },
  1646. { path: '/things/b.txt', type: 'doc' },
  1647. ],
  1648. })
  1649. expect(this.ProjectGetter.getProject.callCount).to.equal(1)
  1650. expect(
  1651. this.ProjectEntityHandler.getAllEntitiesFromProject.callCount
  1652. ).to.equal(1)
  1653. done()
  1654. }
  1655. this.ProjectController.projectEntitiesJson(this.req, this.res, this.next)
  1656. })
  1657. })
  1658. describe('_buildProjectViewModel', function () {
  1659. beforeEach(function () {
  1660. this.ProjectHelper.isArchived.returns(false)
  1661. this.ProjectHelper.isTrashed.returns(false)
  1662. this.project = {
  1663. _id: 'abcd',
  1664. name: 'netsenits',
  1665. lastUpdated: 1,
  1666. lastUpdatedBy: 2,
  1667. publicAccesLevel: 'private',
  1668. archived: false,
  1669. owner_ref: 'defg',
  1670. tokens: {
  1671. readAndWrite: '1abcd',
  1672. readAndWritePrefix: '1',
  1673. readOnly: 'neiotsranteoia',
  1674. },
  1675. }
  1676. })
  1677. describe('project not being archived or trashed', function () {
  1678. it('should produce a model of the project', function () {
  1679. const result = this.ProjectController._buildProjectViewModel(
  1680. this.project,
  1681. 'readAndWrite',
  1682. 'owner',
  1683. this.user._id
  1684. )
  1685. expect(result).to.exist
  1686. expect(result).to.be.an('object')
  1687. expect(result).to.deep.equal({
  1688. id: 'abcd',
  1689. name: 'netsenits',
  1690. lastUpdated: 1,
  1691. lastUpdatedBy: 2,
  1692. publicAccessLevel: 'private',
  1693. accessLevel: 'readAndWrite',
  1694. source: 'owner',
  1695. archived: false,
  1696. trashed: false,
  1697. owner_ref: 'defg',
  1698. isV1Project: false,
  1699. })
  1700. })
  1701. })
  1702. describe('project being simultaneously archived and trashed', function () {
  1703. beforeEach(function () {
  1704. this.ProjectHelper.isArchived.returns(true)
  1705. this.ProjectHelper.isTrashed.returns(true)
  1706. })
  1707. it('should produce a model of the project', function () {
  1708. const result = this.ProjectController._buildProjectViewModel(
  1709. this.project,
  1710. 'readAndWrite',
  1711. 'owner',
  1712. this.user._id
  1713. )
  1714. expect(result).to.exist
  1715. expect(result).to.be.an('object')
  1716. expect(result).to.deep.equal({
  1717. id: 'abcd',
  1718. name: 'netsenits',
  1719. lastUpdated: 1,
  1720. lastUpdatedBy: 2,
  1721. publicAccessLevel: 'private',
  1722. accessLevel: 'readAndWrite',
  1723. source: 'owner',
  1724. archived: true,
  1725. trashed: false,
  1726. owner_ref: 'defg',
  1727. isV1Project: false,
  1728. })
  1729. })
  1730. })
  1731. describe('when token-read-only access', function () {
  1732. it('should redact the owner and last-updated data', function () {
  1733. const result = this.ProjectController._buildProjectViewModel(
  1734. this.project,
  1735. 'readOnly',
  1736. 'token',
  1737. this.user._id
  1738. )
  1739. expect(result).to.exist
  1740. expect(result).to.be.an('object')
  1741. expect(result).to.deep.equal({
  1742. id: 'abcd',
  1743. name: 'netsenits',
  1744. lastUpdated: 1,
  1745. lastUpdatedBy: null,
  1746. publicAccessLevel: 'private',
  1747. accessLevel: 'readOnly',
  1748. source: 'token',
  1749. archived: false,
  1750. trashed: false,
  1751. owner_ref: null,
  1752. isV1Project: false,
  1753. })
  1754. })
  1755. })
  1756. })
  1757. describe('_isInPercentageRollout', function () {
  1758. before(function () {
  1759. this.ids = [
  1760. '5a05cd7621f9fe22be131740',
  1761. '5a05cd7821f9fe22be131741',
  1762. '5a05cd7921f9fe22be131742',
  1763. '5a05cd7a21f9fe22be131743',
  1764. '5a05cd7b21f9fe22be131744',
  1765. '5a05cd7c21f9fe22be131745',
  1766. '5a05cd7d21f9fe22be131746',
  1767. '5a05cd7e21f9fe22be131747',
  1768. '5a05cd7f21f9fe22be131748',
  1769. '5a05cd8021f9fe22be131749',
  1770. '5a05cd8021f9fe22be13174a',
  1771. '5a05cd8121f9fe22be13174b',
  1772. '5a05cd8221f9fe22be13174c',
  1773. '5a05cd8221f9fe22be13174d',
  1774. '5a05cd8321f9fe22be13174e',
  1775. '5a05cd8321f9fe22be13174f',
  1776. '5a05cd8421f9fe22be131750',
  1777. '5a05cd8421f9fe22be131751',
  1778. '5a05cd8421f9fe22be131752',
  1779. '5a05cd8521f9fe22be131753',
  1780. ]
  1781. })
  1782. it('should produce the expected results', function () {
  1783. expect(
  1784. this.ids.map(i =>
  1785. this.ProjectController._isInPercentageRollout('abcd', i, 50)
  1786. )
  1787. ).to.deep.equal([
  1788. false,
  1789. false,
  1790. false,
  1791. false,
  1792. false,
  1793. false,
  1794. true,
  1795. false,
  1796. true,
  1797. true,
  1798. true,
  1799. true,
  1800. true,
  1801. true,
  1802. false,
  1803. false,
  1804. false,
  1805. true,
  1806. false,
  1807. true,
  1808. ])
  1809. expect(
  1810. this.ids.map(i =>
  1811. this.ProjectController._isInPercentageRollout('efgh', i, 50)
  1812. )
  1813. ).to.deep.equal([
  1814. false,
  1815. false,
  1816. false,
  1817. false,
  1818. true,
  1819. false,
  1820. false,
  1821. true,
  1822. false,
  1823. false,
  1824. true,
  1825. true,
  1826. true,
  1827. false,
  1828. true,
  1829. false,
  1830. true,
  1831. true,
  1832. false,
  1833. false,
  1834. ])
  1835. })
  1836. })
  1837. })