ProjectDetailsHandler.test.mjs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. import { vi, expect } from 'vitest'
  2. import sinon from 'sinon'
  3. import mongodb from 'mongodb-legacy'
  4. import Errors from '../../../../app/src/Features/Errors/Errors.js'
  5. import ProjectHelper from '../../../../app/src/Features/Project/ProjectHelper.js'
  6. const { ObjectId } = mongodb
  7. const MODULE_PATH = '../../../../app/src/Features/Project/ProjectDetailsHandler'
  8. vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
  9. vi.importActual('../../../../app/src/Features/Errors/Errors.js')
  10. )
  11. describe('ProjectDetailsHandler', function () {
  12. beforeEach(async function (ctx) {
  13. ctx.user = {
  14. _id: new ObjectId(),
  15. email: 'user@example.com',
  16. features: 'mock-features',
  17. }
  18. ctx.collaborator = {
  19. _id: new ObjectId(),
  20. email: 'collaborator@example.com',
  21. }
  22. ctx.project = {
  23. _id: new ObjectId(),
  24. name: 'project',
  25. description: 'this is a great project',
  26. something: 'should not exist',
  27. compiler: 'latexxxxxx',
  28. owner_ref: ctx.user._id,
  29. collaberator_refs: [ctx.collaborator._id],
  30. }
  31. ctx.ProjectGetter = {
  32. promises: {
  33. getProjectWithoutDocLines: sinon.stub().resolves(ctx.project),
  34. getProject: sinon.stub().resolves(ctx.project),
  35. findAllUsersProjects: sinon.stub().resolves({
  36. owned: [],
  37. readAndWrite: [],
  38. readOnly: [],
  39. tokenReadAndWrite: [],
  40. tokenReadOnly: [],
  41. }),
  42. },
  43. }
  44. ctx.ProjectModelUpdateQuery = {
  45. exec: sinon.stub().resolves(),
  46. }
  47. ctx.ProjectModel = {
  48. updateOne: sinon.stub().returns(ctx.ProjectModelUpdateQuery),
  49. }
  50. ctx.UserGetter = {
  51. promises: {
  52. getUser: sinon.stub().resolves(ctx.user),
  53. },
  54. }
  55. ctx.TpdsUpdateSender = {
  56. promises: {
  57. moveEntity: sinon.stub().resolves(),
  58. },
  59. }
  60. ctx.TokenGenerator = {
  61. readAndWriteToken: sinon.stub(),
  62. promises: {
  63. generateUniqueReadOnlyToken: sinon.stub(),
  64. },
  65. }
  66. ctx.settings = {
  67. defaultFeatures: 'default-features',
  68. }
  69. vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({
  70. default: ProjectHelper,
  71. }))
  72. vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
  73. default: ctx.ProjectGetter,
  74. }))
  75. vi.doMock('../../../../app/src/models/Project', () => ({
  76. Project: ctx.ProjectModel,
  77. }))
  78. vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
  79. default: ctx.UserGetter,
  80. }))
  81. vi.doMock(
  82. '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender',
  83. () => ({
  84. default: ctx.TpdsUpdateSender,
  85. })
  86. )
  87. vi.doMock(
  88. '../../../../app/src/Features/TokenGenerator/TokenGenerator',
  89. () => ({
  90. default: ctx.TokenGenerator,
  91. })
  92. )
  93. vi.doMock('@overleaf/settings', () => ({
  94. default: ctx.settings,
  95. }))
  96. ctx.handler = (await import(MODULE_PATH)).default
  97. })
  98. describe('getDetails', function () {
  99. it('should find the project and owner', async function (ctx) {
  100. const details = await ctx.handler.promises.getDetails(ctx.project._id)
  101. details.name.should.equal(ctx.project.name)
  102. details.description.should.equal(ctx.project.description)
  103. details.compiler.should.equal(ctx.project.compiler)
  104. details.features.should.equal(ctx.user.features)
  105. expect(details.something).to.be.undefined
  106. })
  107. it('should find overleaf metadata if it exists', async function (ctx) {
  108. ctx.project.overleaf = { id: 'id' }
  109. const details = await ctx.handler.promises.getDetails(ctx.project._id)
  110. details.overleaf.should.equal(ctx.project.overleaf)
  111. expect(details.something).to.be.undefined
  112. })
  113. it('should return an error for a non-existent project', async function (ctx) {
  114. ctx.ProjectGetter.promises.getProject.resolves(null)
  115. await expect(
  116. ctx.handler.promises.getDetails('0123456789012345678901234')
  117. ).to.be.rejectedWith(Errors.NotFoundError)
  118. })
  119. it('should return the default features if no owner found', async function (ctx) {
  120. ctx.UserGetter.promises.getUser.resolves(null)
  121. const details = await ctx.handler.promises.getDetails(ctx.project._id)
  122. details.features.should.equal(ctx.settings.defaultFeatures)
  123. })
  124. it('should rethrow any error', async function (ctx) {
  125. ctx.ProjectGetter.promises.getProject.rejects(new Error('boom'))
  126. await expect(ctx.handler.promises.getDetails(ctx.project._id)).to.be
  127. .rejected
  128. })
  129. })
  130. describe('getProjectDescription', function () {
  131. it('should make a call to mongo just for the description', async function (ctx) {
  132. ctx.ProjectGetter.promises.getProject.resolves()
  133. await ctx.handler.promises.getProjectDescription(ctx.project._id)
  134. expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledWith(
  135. ctx.project._id,
  136. { description: true }
  137. )
  138. })
  139. it('should return what the mongo call returns', async function (ctx) {
  140. const expectedDescription = 'cool project'
  141. ctx.ProjectGetter.promises.getProject.resolves({
  142. description: expectedDescription,
  143. })
  144. const description = await ctx.handler.promises.getProjectDescription(
  145. ctx.project._id
  146. )
  147. expect(description).to.equal(expectedDescription)
  148. })
  149. })
  150. describe('setProjectDescription', function () {
  151. beforeEach(function (ctx) {
  152. ctx.description = 'updated teh description'
  153. })
  154. it('should update the project detials', async function (ctx) {
  155. await ctx.handler.promises.setProjectDescription(
  156. ctx.project._id,
  157. ctx.description
  158. )
  159. expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
  160. { _id: ctx.project._id },
  161. { description: ctx.description }
  162. )
  163. })
  164. })
  165. describe('renameProject', function () {
  166. beforeEach(function (ctx) {
  167. ctx.newName = 'new name here'
  168. })
  169. it('should update the project with the new name', async function (ctx) {
  170. await ctx.handler.promises.renameProject(ctx.project._id, ctx.newName)
  171. expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
  172. { _id: ctx.project._id },
  173. { name: ctx.newName }
  174. )
  175. })
  176. it('should tell the TpdsUpdateSender', async function (ctx) {
  177. await ctx.handler.promises.renameProject(ctx.project._id, ctx.newName)
  178. expect(ctx.TpdsUpdateSender.promises.moveEntity).to.have.been.calledWith({
  179. projectId: ctx.project._id,
  180. projectName: ctx.project.name,
  181. newProjectName: ctx.newName,
  182. })
  183. })
  184. it('should not do anything with an invalid name', async function (ctx) {
  185. await expect(ctx.handler.promises.renameProject(ctx.project._id)).to.be
  186. .rejected
  187. expect(ctx.TpdsUpdateSender.promises.moveEntity).not.to.have.been.called
  188. expect(ctx.ProjectModel.updateOne).not.to.have.been.called
  189. })
  190. it('should trim whitespace around name', async function (ctx) {
  191. await ctx.handler.promises.renameProject(
  192. ctx.project._id,
  193. ` ${ctx.newName} `
  194. )
  195. expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
  196. { _id: ctx.project._id },
  197. { name: ctx.newName }
  198. )
  199. })
  200. })
  201. describe('validateProjectName', function () {
  202. it('should reject undefined names', async function (ctx) {
  203. await expect(ctx.handler.promises.validateProjectName(undefined)).to.be
  204. .rejected
  205. })
  206. it('should reject empty names', async function (ctx) {
  207. await expect(ctx.handler.promises.validateProjectName('')).to.be.rejected
  208. })
  209. it('should reject names with /s', async function (ctx) {
  210. await expect(ctx.handler.promises.validateProjectName('foo/bar')).to.be
  211. .rejected
  212. })
  213. it('should reject names with \\s', async function (ctx) {
  214. await expect(ctx.handler.promises.validateProjectName('foo\\bar')).to.be
  215. .rejected
  216. })
  217. it('should reject long names', async function (ctx) {
  218. await expect(ctx.handler.promises.validateProjectName('a'.repeat(1000)))
  219. .to.be.rejected
  220. })
  221. it('should accept normal names', async function (ctx) {
  222. await expect(ctx.handler.promises.validateProjectName('foobar')).to.be
  223. .fulfilled
  224. })
  225. })
  226. describe('generateUniqueName', function () {
  227. // actually testing `ProjectHelper.promises.ensureNameIsUnique()`
  228. beforeEach(function (ctx) {
  229. ctx.longName = 'x'.repeat(ctx.handler.MAX_PROJECT_NAME_LENGTH - 5)
  230. const usersProjects = {
  231. owned: [
  232. { _id: 1, name: 'name' },
  233. { _id: 2, name: 'name1' },
  234. { _id: 3, name: 'name11' },
  235. { _id: 100, name: 'numeric' },
  236. { _id: 101, name: 'numeric (1)' },
  237. { _id: 102, name: 'numeric (2)' },
  238. { _id: 103, name: 'numeric (3)' },
  239. { _id: 104, name: 'numeric (4)' },
  240. { _id: 105, name: 'numeric (5)' },
  241. { _id: 106, name: 'numeric (6)' },
  242. { _id: 107, name: 'numeric (7)' },
  243. { _id: 108, name: 'numeric (8)' },
  244. { _id: 109, name: 'numeric (9)' },
  245. { _id: 110, name: 'numeric (10)' },
  246. { _id: 111, name: 'numeric (11)' },
  247. { _id: 112, name: 'numeric (12)' },
  248. { _id: 113, name: 'numeric (13)' },
  249. { _id: 114, name: 'numeric (14)' },
  250. { _id: 115, name: 'numeric (15)' },
  251. { _id: 116, name: 'numeric (16)' },
  252. { _id: 117, name: 'numeric (17)' },
  253. { _id: 118, name: 'numeric (18)' },
  254. { _id: 119, name: 'numeric (19)' },
  255. { _id: 120, name: 'numeric (20)' },
  256. { _id: 130, name: 'numeric (30)' },
  257. { _id: 131, name: 'numeric (31)' },
  258. { _id: 132, name: 'numeric (32)' },
  259. { _id: 133, name: 'numeric (33)' },
  260. { _id: 134, name: 'numeric (34)' },
  261. { _id: 135, name: 'numeric (35)' },
  262. { _id: 136, name: 'numeric (36)' },
  263. { _id: 137, name: 'numeric (37)' },
  264. { _id: 138, name: 'numeric (38)' },
  265. { _id: 139, name: 'numeric (39)' },
  266. { _id: 140, name: 'numeric (40)' },
  267. { _id: 141, name: 'Yearbook (2021)' },
  268. { _id: 142, name: 'Yearbook (2021) (1)' },
  269. { _id: 143, name: 'Resume (2020' },
  270. ],
  271. readAndWrite: [
  272. { _id: 4, name: 'name2' },
  273. { _id: 5, name: 'name22' },
  274. ],
  275. readOnly: [
  276. { _id: 6, name: 'name3' },
  277. { _id: 7, name: 'name33' },
  278. ],
  279. tokenReadAndWrite: [
  280. { _id: 8, name: 'name4' },
  281. { _id: 9, name: 'name44' },
  282. ],
  283. tokenReadOnly: [
  284. { _id: 10, name: 'name5' },
  285. { _id: 11, name: 'name55' },
  286. { _id: 12, name: ctx.longName },
  287. ],
  288. }
  289. ctx.ProjectGetter.promises.findAllUsersProjects.resolves(usersProjects)
  290. })
  291. it('should leave a unique name unchanged', async function (ctx) {
  292. const name = await ctx.handler.promises.generateUniqueName(
  293. ctx.user._id,
  294. 'unique-name',
  295. ['-test-suffix']
  296. )
  297. expect(name).to.equal('unique-name')
  298. })
  299. it('should append a suffix to an existing name', async function (ctx) {
  300. const name = await ctx.handler.promises.generateUniqueName(
  301. ctx.user._id,
  302. 'name1',
  303. ['-test-suffix']
  304. )
  305. expect(name).to.equal('name1-test-suffix')
  306. })
  307. it('should fallback to a second suffix when needed', async function (ctx) {
  308. const name = await ctx.handler.promises.generateUniqueName(
  309. ctx.user._id,
  310. 'name1',
  311. ['1', '-test-suffix']
  312. )
  313. expect(name).to.equal('name1-test-suffix')
  314. })
  315. it('should truncate the name when append a suffix if the result is too long', async function (ctx) {
  316. const name = await ctx.handler.promises.generateUniqueName(
  317. ctx.user._id,
  318. ctx.longName,
  319. ['-test-suffix']
  320. )
  321. expect(name).to.equal(
  322. ctx.longName.substr(0, ctx.handler.MAX_PROJECT_NAME_LENGTH - 12) +
  323. '-test-suffix'
  324. )
  325. })
  326. it('should use a numeric index if no suffix is supplied', async function (ctx) {
  327. const name = await ctx.handler.promises.generateUniqueName(
  328. ctx.user._id,
  329. 'name1',
  330. []
  331. )
  332. expect(name).to.equal('name1 (1)')
  333. })
  334. it('should use a numeric index if all suffixes are exhausted', async function (ctx) {
  335. const name = await ctx.handler.promises.generateUniqueName(
  336. ctx.user._id,
  337. 'name',
  338. ['1', '11']
  339. )
  340. expect(name).to.equal('name (1)')
  341. })
  342. it('should find the next lowest available numeric index for the base name', async function (ctx) {
  343. const name = await ctx.handler.promises.generateUniqueName(
  344. ctx.user._id,
  345. 'numeric',
  346. []
  347. )
  348. expect(name).to.equal('numeric (21)')
  349. })
  350. it('should not find a numeric index lower than the one already present', async function (ctx) {
  351. const name = await ctx.handler.promises.generateUniqueName(
  352. ctx.user._id,
  353. 'numeric (31)',
  354. []
  355. )
  356. expect(name).to.equal('numeric (41)')
  357. })
  358. it('should handle years in name', async function (ctx) {
  359. const name = await ctx.handler.promises.generateUniqueName(
  360. ctx.user._id,
  361. 'unique-name (2021)',
  362. []
  363. )
  364. expect(name).to.equal('unique-name (2021)')
  365. })
  366. it('should handle duplicating with year in name', async function (ctx) {
  367. const name = await ctx.handler.promises.generateUniqueName(
  368. ctx.user._id,
  369. 'Yearbook (2021)',
  370. []
  371. )
  372. expect(name).to.equal('Yearbook (2021) (2)')
  373. })
  374. describe('title with that causes invalid regex', function () {
  375. it('should create the project with a suffix when project name exists', async function (ctx) {
  376. const name = await ctx.handler.promises.generateUniqueName(
  377. ctx.user._id,
  378. 'Resume (2020',
  379. []
  380. )
  381. expect(name).to.equal('Resume (2020 (1)')
  382. })
  383. it('should create the project with the provided name', async function (ctx) {
  384. const name = await ctx.handler.promises.generateUniqueName(
  385. ctx.user._id,
  386. 'Yearbook (2021',
  387. []
  388. )
  389. expect(name).to.equal('Yearbook (2021')
  390. })
  391. })
  392. describe('numeric index is already present', function () {
  393. describe('when there is 1 project "x (2)"', function () {
  394. beforeEach(function (ctx) {
  395. const usersProjects = {
  396. owned: [{ _id: 1, name: 'x (2)' }],
  397. }
  398. ctx.ProjectGetter.promises.findAllUsersProjects.resolves(
  399. usersProjects
  400. )
  401. })
  402. it('should produce "x (3)" uploading a zip with name "x (2)"', async function (ctx) {
  403. const name = await ctx.handler.promises.generateUniqueName(
  404. ctx.user._id,
  405. 'x (2)',
  406. []
  407. )
  408. expect(name).to.equal('x (3)')
  409. })
  410. })
  411. describe('when there are 2 projects "x (2)" and "x (3)"', function () {
  412. beforeEach(function (ctx) {
  413. const usersProjects = {
  414. owned: [
  415. { _id: 1, name: 'x (2)' },
  416. { _id: 2, name: 'x (3)' },
  417. ],
  418. }
  419. ctx.ProjectGetter.promises.findAllUsersProjects.resolves(
  420. usersProjects
  421. )
  422. })
  423. it('should produce "x (4)" when uploading a zip with name "x (2)"', async function (ctx) {
  424. const name = await ctx.handler.promises.generateUniqueName(
  425. ctx.user._id,
  426. 'x (2)',
  427. []
  428. )
  429. expect(name).to.equal('x (4)')
  430. })
  431. })
  432. describe('when there are 2 projects "x (2)" and "x (4)"', function () {
  433. beforeEach(function (ctx) {
  434. const usersProjects = {
  435. owned: [
  436. { _id: 1, name: 'x (2)' },
  437. { _id: 2, name: 'x (4)' },
  438. ],
  439. }
  440. ctx.ProjectGetter.promises.findAllUsersProjects.resolves(
  441. usersProjects
  442. )
  443. })
  444. it('should produce "x (3)" when uploading a zip with name "x (2)"', async function (ctx) {
  445. const name = await ctx.handler.promises.generateUniqueName(
  446. ctx.user._id,
  447. 'x (2)',
  448. []
  449. )
  450. expect(name).to.equal('x (3)')
  451. })
  452. it('should produce "x (5)" when uploading a zip with name "x (4)"', async function (ctx) {
  453. const name = await ctx.handler.promises.generateUniqueName(
  454. ctx.user._id,
  455. 'x (4)',
  456. []
  457. )
  458. expect(name).to.equal('x (5)')
  459. })
  460. })
  461. })
  462. })
  463. describe('fixProjectName', function () {
  464. it('should change empty names to Untitled', function (ctx) {
  465. expect(ctx.handler.fixProjectName('')).to.equal('Untitled')
  466. })
  467. it('should replace / with -', function (ctx) {
  468. expect(ctx.handler.fixProjectName('foo/bar')).to.equal('foo-bar')
  469. })
  470. it("should replace \\ with ''", function (ctx) {
  471. expect(ctx.handler.fixProjectName('foo \\ bar')).to.equal('foo bar')
  472. })
  473. it('should truncate long names', function (ctx) {
  474. expect(ctx.handler.fixProjectName('a'.repeat(1000))).to.equal(
  475. 'a'.repeat(150)
  476. )
  477. })
  478. it('should accept normal names', function (ctx) {
  479. expect(ctx.handler.fixProjectName('foobar')).to.equal('foobar')
  480. })
  481. it('should trim name after truncation', function (ctx) {
  482. expect(ctx.handler.fixProjectName('a'.repeat(149) + ' a')).to.equal(
  483. 'a'.repeat(149)
  484. )
  485. })
  486. })
  487. describe('setPublicAccessLevel', function () {
  488. beforeEach(function (ctx) {
  489. ctx.accessLevel = 'tokenBased'
  490. })
  491. it('should update the project with the new level', async function (ctx) {
  492. await ctx.handler.promises.setPublicAccessLevel(
  493. ctx.project._id,
  494. ctx.accessLevel
  495. )
  496. expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
  497. { _id: ctx.project._id },
  498. { publicAccesLevel: ctx.accessLevel }
  499. )
  500. })
  501. it('should not produce an error', async function (ctx) {
  502. await expect(
  503. ctx.handler.promises.setPublicAccessLevel(
  504. ctx.project._id,
  505. ctx.accessLevel
  506. )
  507. ).to.be.fulfilled
  508. })
  509. describe('when update produces an error', function () {
  510. beforeEach(function (ctx) {
  511. ctx.ProjectModelUpdateQuery.exec.rejects(new Error('woops'))
  512. })
  513. it('should produce an error', async function (ctx) {
  514. await expect(
  515. ctx.handler.promises.setPublicAccessLevel(
  516. ctx.project._id,
  517. ctx.accessLevel
  518. )
  519. ).to.be.rejected
  520. })
  521. })
  522. })
  523. describe('ensureTokensArePresent', function () {
  524. describe('when the project has tokens', function () {
  525. beforeEach(function (ctx) {
  526. ctx.project = {
  527. _id: ctx.project._id,
  528. tokens: {
  529. readOnly: 'aaa',
  530. readAndWrite: '42bbb',
  531. readAndWritePrefix: '42',
  532. },
  533. }
  534. ctx.ProjectGetter.promises.getProject.resolves(ctx.project)
  535. })
  536. it('should get the project', async function (ctx) {
  537. await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
  538. expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledOnce
  539. expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledWith(
  540. ctx.project._id,
  541. {
  542. tokens: 1,
  543. }
  544. )
  545. })
  546. it('should not update the project with new tokens', async function (ctx) {
  547. await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
  548. expect(ctx.ProjectModel.updateOne).not.to.have.been.called
  549. })
  550. })
  551. describe('when tokens are missing', function () {
  552. beforeEach(function (ctx) {
  553. ctx.project = { _id: ctx.project._id }
  554. ctx.ProjectGetter.promises.getProject.resolves(ctx.project)
  555. ctx.readOnlyToken = 'abc'
  556. ctx.readAndWriteToken = '42def'
  557. ctx.readAndWriteTokenPrefix = '42'
  558. ctx.TokenGenerator.promises.generateUniqueReadOnlyToken.resolves(
  559. ctx.readOnlyToken
  560. )
  561. ctx.TokenGenerator.readAndWriteToken.returns({
  562. token: ctx.readAndWriteToken,
  563. numericPrefix: ctx.readAndWriteTokenPrefix,
  564. })
  565. })
  566. it('should get the project', async function (ctx) {
  567. await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
  568. expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledOnce
  569. expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledWith(
  570. ctx.project._id,
  571. {
  572. tokens: 1,
  573. }
  574. )
  575. })
  576. it('should update the project with new tokens', async function (ctx) {
  577. await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
  578. expect(ctx.TokenGenerator.promises.generateUniqueReadOnlyToken).to.have
  579. .been.calledOnce
  580. expect(ctx.TokenGenerator.readAndWriteToken).to.have.been.calledOnce
  581. expect(ctx.ProjectModel.updateOne).to.have.been.calledOnce
  582. expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
  583. { _id: ctx.project._id },
  584. {
  585. $set: {
  586. tokens: {
  587. readOnly: ctx.readOnlyToken,
  588. readAndWrite: ctx.readAndWriteToken,
  589. readAndWritePrefix: ctx.readAndWriteTokenPrefix,
  590. },
  591. },
  592. }
  593. )
  594. })
  595. })
  596. })
  597. describe('clearTokens', function () {
  598. it('clears the tokens from the project', async function (ctx) {
  599. await ctx.handler.promises.clearTokens(ctx.project._id)
  600. expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
  601. { _id: ctx.project._id },
  602. { $unset: { tokens: 1 }, $set: { publicAccesLevel: 'private' } }
  603. )
  604. })
  605. })
  606. })