EditorHttpController.test.mjs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  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 MockRequest from '../helpers/MockRequest.js'
  6. import MockResponse from '../helpers/MockResponse.js'
  7. const { ObjectId } = mongodb
  8. const MODULE_PATH =
  9. '../../../../app/src/Features/Editor/EditorHttpController.mjs'
  10. describe('EditorHttpController', function () {
  11. beforeEach(async function (ctx) {
  12. ctx.ownerId = new ObjectId()
  13. ctx.project = {
  14. _id: new ObjectId(),
  15. owner_ref: ctx.ownerId,
  16. }
  17. ctx.user = {
  18. _id: new ObjectId(),
  19. projects: {},
  20. }
  21. ctx.members = [
  22. { user: { _id: 'owner', features: {} }, privilegeLevel: 'owner' },
  23. { user: { _id: 'one' }, privilegeLevel: 'readOnly' },
  24. ]
  25. ctx.ownerMember = ctx.members[0]
  26. ctx.invites = [{ _id: 'three' }, { _id: 'four' }]
  27. ctx.projectView = {
  28. _id: ctx.project._id,
  29. owner: {
  30. _id: 'owner',
  31. email: 'owner@example.com',
  32. other_property: true,
  33. },
  34. members: [
  35. { _id: 'owner', privileges: 'owner' },
  36. { _id: 'one', privileges: 'readOnly' },
  37. ],
  38. invites: [{ three: 3 }, { four: 4 }],
  39. }
  40. ctx.reducedProjectView = {
  41. _id: ctx.projectView._id,
  42. owner: { _id: ctx.projectView.owner._id },
  43. members: [],
  44. invites: [],
  45. }
  46. ctx.doc = { _id: new ObjectId(), name: 'excellent-original-idea.tex' }
  47. ctx.file = { _id: new ObjectId() }
  48. ctx.folder = { _id: new ObjectId() }
  49. ctx.source = 'editor'
  50. ctx.parentFolderId = 'mock-folder-id'
  51. ctx.req = new MockRequest()
  52. ctx.res = new MockResponse()
  53. ctx.next = sinon.stub()
  54. ctx.token = null
  55. ctx.docLines = ['hello', 'overleaf']
  56. ctx.AuthorizationManager = {
  57. isRestrictedUser: sinon.stub().returns(false),
  58. promises: {
  59. getPrivilegeLevelForProjectWithProjectAccess: sinon
  60. .stub()
  61. .resolves('owner'),
  62. },
  63. }
  64. const members = ctx.members
  65. const ownerMember = ctx.ownerMember
  66. ctx.CollaboratorsGetter = {
  67. ProjectAccess: class {
  68. loadOwnerAndInvitedMembers() {
  69. return { members, ownerMember }
  70. }
  71. loadOwner() {
  72. return ownerMember
  73. }
  74. isUserTokenMember() {
  75. return false
  76. }
  77. isUserInvitedMember() {
  78. return false
  79. }
  80. },
  81. promises: {
  82. isUserInvitedMemberOfProject: sinon.stub().resolves(false),
  83. },
  84. }
  85. ctx.CollaboratorsHandler = {
  86. promises: {
  87. userIsTokenMember: sinon.stub().resolves(false),
  88. },
  89. }
  90. ctx.invites = [
  91. {
  92. _id: 'invite_one',
  93. email: 'user-one@example.com',
  94. privileges: 'readOnly',
  95. projectId: ctx.project._id,
  96. },
  97. {
  98. _id: 'invite_two',
  99. email: 'user-two@example.com',
  100. privileges: 'readOnly',
  101. projectId: ctx.project._id,
  102. },
  103. ]
  104. ctx.CollaboratorsInviteGetter = {
  105. promises: {
  106. getAllInvites: sinon.stub().resolves(ctx.invites),
  107. },
  108. }
  109. ctx.EditorController = {
  110. promises: {
  111. addDoc: sinon.stub().resolves(ctx.doc),
  112. addFile: sinon.stub().resolves(ctx.file),
  113. addFolder: sinon.stub().resolves(ctx.folder),
  114. renameEntity: sinon.stub().resolves(),
  115. moveEntity: sinon.stub().resolves(),
  116. deleteEntity: sinon.stub().resolves(),
  117. },
  118. }
  119. ctx.ProjectDeleter = {
  120. promises: {
  121. unmarkAsDeletedByExternalSource: sinon.stub().resolves(),
  122. },
  123. }
  124. ctx.ProjectGetter = {
  125. promises: {
  126. getProjectWithoutDocLines: sinon.stub().resolves(ctx.project),
  127. },
  128. }
  129. ctx.ProjectEditorHandler = {
  130. buildProjectModelView: sinon.stub().returns(ctx.projectView),
  131. }
  132. ctx.Metrics = { inc: sinon.stub() }
  133. ctx.TokenAccessHandler = {
  134. getRequestToken: sinon.stub().returns(ctx.token),
  135. }
  136. ctx.SessionManager = {
  137. getLoggedInUserId: sinon.stub().returns(ctx.user._id),
  138. }
  139. ctx.ProjectEntityUpdateHandler = {
  140. promises: {
  141. convertDocToFile: sinon.stub().resolves(ctx.file),
  142. },
  143. }
  144. ctx.DocstoreManager = {
  145. promises: {
  146. getAllDeletedDocs: sinon.stub().resolves([]),
  147. },
  148. }
  149. ctx.HttpErrorHandler = {
  150. notFound: sinon.stub(),
  151. unprocessableEntity: sinon.stub(),
  152. }
  153. ctx.SplitTestHandler = {
  154. promises: {
  155. getAssignmentForUser: sinon.stub().resolves({ variant: 'default' }),
  156. },
  157. }
  158. ctx.UserGetter = { promises: { getUser: sinon.stub().resolves(null, {}) } }
  159. vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
  160. vi.importActual('../../../../app/src/Features/Errors/Errors.js')
  161. )
  162. vi.doMock('../../../../app/src/Features/Project/ProjectDeleter.js', () => ({
  163. default: ctx.ProjectDeleter,
  164. }))
  165. vi.doMock('../../../../app/src/Features/Project/ProjectGetter.js', () => ({
  166. default: ctx.ProjectGetter,
  167. }))
  168. vi.doMock(
  169. '../../../../app/src/Features/Authorization/AuthorizationManager.js',
  170. () => ({
  171. default: ctx.AuthorizationManager,
  172. })
  173. )
  174. vi.doMock(
  175. '../../../../app/src/Features/Project/ProjectEditorHandler.js',
  176. () => ({
  177. default: ctx.ProjectEditorHandler,
  178. })
  179. )
  180. vi.doMock(
  181. '../../../../app/src/Features/Editor/EditorController.js',
  182. () => ({
  183. default: ctx.EditorController,
  184. })
  185. )
  186. vi.doMock('@overleaf/metrics', () => ({
  187. default: ctx.Metrics,
  188. }))
  189. vi.doMock(
  190. '../../../../app/src/Features/Collaborators/CollaboratorsGetter.js',
  191. () => ({
  192. default: ctx.CollaboratorsGetter,
  193. })
  194. )
  195. vi.doMock(
  196. '../../../../app/src/Features/Collaborators/CollaboratorsHandler.js',
  197. () => ({
  198. default: ctx.CollaboratorsHandler,
  199. })
  200. )
  201. vi.doMock(
  202. '../../../../app/src/Features/Collaborators/CollaboratorsInviteGetter.js',
  203. () => ({
  204. default: ctx.CollaboratorsInviteGetter,
  205. })
  206. )
  207. vi.doMock(
  208. '../../../../app/src/Features/TokenAccess/TokenAccessHandler.js',
  209. () => ({
  210. default: ctx.TokenAccessHandler,
  211. })
  212. )
  213. vi.doMock(
  214. '../../../../app/src/Features/Authentication/SessionManager.js',
  215. () => ({
  216. default: ctx.SessionManager,
  217. })
  218. )
  219. vi.doMock('../../../../app/src/infrastructure/FileWriter.js', () => ({
  220. default: ctx.FileWriter,
  221. }))
  222. vi.doMock(
  223. '../../../../app/src/Features/Project/ProjectEntityUpdateHandler.js',
  224. () => ({
  225. default: ctx.ProjectEntityUpdateHandler,
  226. })
  227. )
  228. vi.doMock(
  229. '../../../../app/src/Features/Docstore/DocstoreManager.js',
  230. () => ({
  231. default: ctx.DocstoreManager,
  232. })
  233. )
  234. vi.doMock(
  235. '../../../../app/src/Features/Errors/HttpErrorHandler.js',
  236. () => ({
  237. default: ctx.HttpErrorHandler,
  238. })
  239. )
  240. vi.doMock(
  241. '../../../../app/src/Features/SplitTests/SplitTestHandler.js',
  242. () => ({
  243. default: ctx.SplitTestHandler,
  244. })
  245. )
  246. vi.doMock('../../../../app/src/Features/Compile/CompileManager.js', () => ({
  247. default: {},
  248. }))
  249. vi.doMock('../../../../app/src/Features/User/UserGetter.js', () => ({
  250. default: ctx.UserGetter,
  251. }))
  252. ctx.EditorHttpController = (await import(MODULE_PATH)).default
  253. })
  254. describe('joinProject', function () {
  255. beforeEach(function (ctx) {
  256. ctx.req.params = { Project_id: ctx.project._id }
  257. ctx.req.query = { user_id: ctx.user._id }
  258. ctx.req.body = { userId: ctx.user._id }
  259. })
  260. describe('successfully', function () {
  261. beforeEach(async function (ctx) {
  262. await new Promise(resolve => {
  263. sinon
  264. .stub(
  265. ctx.CollaboratorsGetter.ProjectAccess.prototype,
  266. 'isUserInvitedMember'
  267. )
  268. .returns(true)
  269. ctx.res.callback = resolve
  270. ctx.EditorHttpController.joinProject(ctx.req, ctx.res)
  271. })
  272. })
  273. it('should request a full view', function (ctx) {
  274. expect(
  275. ctx.ProjectEditorHandler.buildProjectModelView
  276. ).to.have.been.calledWith(
  277. ctx.project,
  278. ctx.ownerMember,
  279. ctx.members,
  280. ctx.invites,
  281. false
  282. )
  283. })
  284. it('should return the project and privilege level', function (ctx) {
  285. expect(ctx.res.json).to.have.been.calledWith({
  286. project: ctx.projectView,
  287. privilegeLevel: 'owner',
  288. isRestrictedUser: false,
  289. isTokenMember: false,
  290. isInvitedMember: true,
  291. })
  292. })
  293. it('should not try to unmark the project as deleted', function (ctx) {
  294. expect(ctx.ProjectDeleter.promises.unmarkAsDeletedByExternalSource).not
  295. .to.have.been.called
  296. })
  297. it('should send an inc metric', function (ctx) {
  298. expect(ctx.Metrics.inc).to.have.been.calledWith('editor.join-project')
  299. })
  300. })
  301. describe('when the project is marked as deleted', function () {
  302. beforeEach(async function (ctx) {
  303. await new Promise(resolve => {
  304. ctx.projectView.deletedByExternalDataSource = true
  305. ctx.res.callback = resolve
  306. ctx.EditorHttpController.joinProject(ctx.req, ctx.res)
  307. })
  308. })
  309. it('should unmark the project as deleted', function (ctx) {
  310. expect(
  311. ctx.ProjectDeleter.promises.unmarkAsDeletedByExternalSource
  312. ).to.have.been.calledWith(ctx.project._id)
  313. })
  314. })
  315. describe('with a restricted user', function () {
  316. beforeEach(async function (ctx) {
  317. await new Promise(resolve => {
  318. ctx.ProjectEditorHandler.buildProjectModelView.returns(
  319. ctx.reducedProjectView
  320. )
  321. ctx.AuthorizationManager.isRestrictedUser.returns(true)
  322. ctx.AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess.resolves(
  323. 'readOnly'
  324. )
  325. ctx.res.callback = resolve
  326. ctx.EditorHttpController.joinProject(ctx.req, ctx.res)
  327. })
  328. })
  329. it('should request a restricted view', function (ctx) {
  330. expect(
  331. ctx.ProjectEditorHandler.buildProjectModelView
  332. ).to.have.been.calledWith(ctx.project, ctx.ownerMember, [], [], true)
  333. })
  334. it('should mark the user as restricted, and hide details of owner', function (ctx) {
  335. expect(ctx.res.json).to.have.been.calledWith({
  336. project: ctx.reducedProjectView,
  337. privilegeLevel: 'readOnly',
  338. isRestrictedUser: true,
  339. isTokenMember: false,
  340. isInvitedMember: false,
  341. })
  342. })
  343. })
  344. describe('when not authorized', function () {
  345. beforeEach(async function (ctx) {
  346. await new Promise(resolve => {
  347. ctx.AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess.resolves(
  348. null
  349. )
  350. ctx.res.callback = resolve
  351. ctx.EditorHttpController.joinProject(ctx.req, ctx.res)
  352. })
  353. })
  354. it('should send a 403 response', function (ctx) {
  355. expect(ctx.res.statusCode).to.equal(403)
  356. })
  357. })
  358. describe('with an anonymous user', function () {
  359. beforeEach(async function (ctx) {
  360. await new Promise(resolve => {
  361. ctx.token = 'token'
  362. ctx.TokenAccessHandler.getRequestToken.returns(ctx.token)
  363. ctx.ProjectEditorHandler.buildProjectModelView.returns(
  364. ctx.reducedProjectView
  365. )
  366. ctx.req.body = {
  367. userId: 'anonymous-user',
  368. anonymousAccessToken: ctx.token,
  369. }
  370. ctx.res.callback = resolve
  371. ctx.AuthorizationManager.isRestrictedUser
  372. .withArgs(null, 'readOnly', false, false)
  373. .returns(true)
  374. ctx.AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess
  375. .withArgs(null, ctx.project._id, ctx.token)
  376. .resolves('readOnly')
  377. ctx.EditorHttpController.joinProject(ctx.req, ctx.res)
  378. })
  379. })
  380. it('should request a restricted view', function (ctx) {
  381. expect(
  382. ctx.ProjectEditorHandler.buildProjectModelView
  383. ).to.have.been.calledWith(ctx.project, ctx.ownerMember, [], [], true)
  384. })
  385. it('should mark the user as restricted', function (ctx) {
  386. expect(ctx.res.json).to.have.been.calledWith({
  387. project: ctx.reducedProjectView,
  388. privilegeLevel: 'readOnly',
  389. isRestrictedUser: true,
  390. isTokenMember: false,
  391. isInvitedMember: false,
  392. })
  393. })
  394. })
  395. describe('with a token access user', function () {
  396. beforeEach(async function (ctx) {
  397. await new Promise(resolve => {
  398. sinon
  399. .stub(
  400. ctx.CollaboratorsGetter.ProjectAccess.prototype,
  401. 'isUserInvitedMember'
  402. )
  403. .returns(false)
  404. sinon
  405. .stub(
  406. ctx.CollaboratorsGetter.ProjectAccess.prototype,
  407. 'isUserTokenMember'
  408. )
  409. .returns(true)
  410. ctx.AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess.resolves(
  411. 'readAndWrite'
  412. )
  413. ctx.res.callback = resolve
  414. ctx.EditorHttpController.joinProject(ctx.req, ctx.res)
  415. })
  416. })
  417. it('should mark the user as being a token-access member', function (ctx) {
  418. expect(ctx.res.json).to.have.been.calledWith({
  419. project: ctx.projectView,
  420. privilegeLevel: 'readAndWrite',
  421. isRestrictedUser: false,
  422. isTokenMember: true,
  423. isInvitedMember: false,
  424. })
  425. })
  426. })
  427. describe('when project is not found', function () {
  428. beforeEach(async function (ctx) {
  429. ctx.ProjectGetter.promises.getProjectWithoutDocLines.resolves(null)
  430. await new Promise(resolve => {
  431. ctx.next.callsFake(() => resolve())
  432. ctx.EditorHttpController.joinProject(ctx.req, ctx.res, ctx.next)
  433. })
  434. })
  435. it('should handle return not found error', function (ctx) {
  436. expect(ctx.next).to.have.been.calledWith(
  437. sinon.match.instanceOf(Errors.NotFoundError)
  438. )
  439. })
  440. })
  441. })
  442. describe('addDoc', function () {
  443. beforeEach(function (ctx) {
  444. ctx.req.params = { Project_id: ctx.project._id }
  445. ctx.req.body = {
  446. name: (ctx.docName = 'doc-name'),
  447. parent_folder_id: ctx.parentFolderId,
  448. }
  449. })
  450. describe('successfully', function () {
  451. beforeEach(async function (ctx) {
  452. await new Promise(resolve => {
  453. ctx.res.callback = resolve
  454. ctx.EditorHttpController.addDoc(ctx.req, ctx.res)
  455. })
  456. })
  457. it('should call EditorController.addDoc', function (ctx) {
  458. expect(ctx.EditorController.promises.addDoc).to.have.been.calledWith(
  459. ctx.project._id,
  460. ctx.parentFolderId,
  461. ctx.docName,
  462. [],
  463. 'editor',
  464. ctx.user._id
  465. )
  466. })
  467. it('should send the doc back as JSON', function (ctx) {
  468. expect(ctx.res.json).to.have.been.calledWith(ctx.doc)
  469. })
  470. })
  471. describe('unsuccesfully', function () {
  472. it('handle name too short', async function (ctx) {
  473. await new Promise(resolve => {
  474. ctx.req.body.name = ''
  475. ctx.res.callback = () => {
  476. expect(ctx.res.statusCode).to.equal(400)
  477. resolve()
  478. }
  479. ctx.EditorHttpController.addDoc(ctx.req, ctx.res)
  480. })
  481. })
  482. it('handle too many files', async function (ctx) {
  483. ctx.EditorController.promises.addDoc.rejects(
  484. new Error('project_has_too_many_files')
  485. )
  486. await new Promise(resolve => {
  487. ctx.res.callback = () => {
  488. expect(ctx.res.body).to.equal('"project_has_too_many_files"')
  489. expect(ctx.res.status).to.have.been.calledWith(400)
  490. resolve()
  491. }
  492. ctx.EditorHttpController.addDoc(ctx.req, ctx.res)
  493. })
  494. })
  495. })
  496. })
  497. describe('addFolder', function () {
  498. beforeEach(function (ctx) {
  499. ctx.folderName = 'folder-name'
  500. ctx.req.params = { Project_id: ctx.project._id }
  501. ctx.req.body = {
  502. name: ctx.folderName,
  503. parent_folder_id: ctx.parentFolderId,
  504. }
  505. })
  506. describe('successfully', function () {
  507. beforeEach(async function (ctx) {
  508. await new Promise(resolve => {
  509. ctx.res.callback = resolve
  510. ctx.EditorHttpController.addFolder(ctx.req, ctx.res)
  511. })
  512. })
  513. it('should call EditorController.addFolder', function (ctx) {
  514. expect(ctx.EditorController.promises.addFolder).to.have.been.calledWith(
  515. ctx.project._id,
  516. ctx.parentFolderId,
  517. ctx.folderName,
  518. 'editor'
  519. )
  520. })
  521. it('should send the folder back as JSON', function (ctx) {
  522. expect(ctx.res.json).to.have.been.calledWith(ctx.folder)
  523. })
  524. })
  525. describe('unsuccesfully', function () {
  526. it('handle name too short', async function (ctx) {
  527. await new Promise(resolve => {
  528. ctx.req.body.name = ''
  529. ctx.res.callback = () => {
  530. expect(ctx.res.statusCode).to.equal(400)
  531. resolve()
  532. }
  533. ctx.EditorHttpController.addFolder(ctx.req, ctx.res)
  534. })
  535. })
  536. it('handle too many files', async function (ctx) {
  537. await new Promise(resolve => {
  538. ctx.EditorController.promises.addFolder.rejects(
  539. new Error('project_has_too_many_files')
  540. )
  541. ctx.res.callback = () => {
  542. expect(ctx.res.body).to.equal('"project_has_too_many_files"')
  543. expect(ctx.res.statusCode).to.equal(400)
  544. resolve()
  545. }
  546. ctx.EditorHttpController.addFolder(ctx.req, ctx.res)
  547. })
  548. })
  549. it('handle invalid element name', async function (ctx) {
  550. await new Promise(resolve => {
  551. ctx.EditorController.promises.addFolder.rejects(
  552. new Error('invalid element name')
  553. )
  554. ctx.res.callback = () => {
  555. expect(ctx.res.body).to.equal('"invalid_file_name"')
  556. expect(ctx.res.statusCode).to.equal(400)
  557. resolve()
  558. }
  559. ctx.EditorHttpController.addFolder(ctx.req, ctx.res)
  560. })
  561. })
  562. })
  563. })
  564. describe('renameEntity', function () {
  565. beforeEach(function (ctx) {
  566. ctx.entityId = 'entity-id-123'
  567. ctx.entityType = 'entity-type'
  568. ctx.req.params = {
  569. Project_id: ctx.project._id,
  570. entity_id: ctx.entityId,
  571. entity_type: ctx.entityType,
  572. }
  573. })
  574. describe('successfully', function () {
  575. beforeEach(async function (ctx) {
  576. await new Promise(resolve => {
  577. ctx.newName = 'new-name'
  578. ctx.req.body = { name: ctx.newName, source: ctx.source }
  579. ctx.res.callback = resolve
  580. ctx.EditorHttpController.renameEntity(ctx.req, ctx.res)
  581. })
  582. })
  583. it('should call EditorController.renameEntity', function (ctx) {
  584. expect(
  585. ctx.EditorController.promises.renameEntity
  586. ).to.have.been.calledWith(
  587. ctx.project._id,
  588. ctx.entityId,
  589. ctx.entityType,
  590. ctx.newName,
  591. ctx.user._id,
  592. ctx.source
  593. )
  594. })
  595. it('should send back a success response', function (ctx) {
  596. expect(ctx.res.sendStatus).to.have.been.calledWith(204)
  597. })
  598. })
  599. describe('with long name', function () {
  600. beforeEach(function (ctx) {
  601. ctx.newName = 'long'.repeat(100)
  602. ctx.req.body = { name: ctx.newName, source: ctx.source }
  603. ctx.EditorHttpController.renameEntity(ctx.req, ctx.res)
  604. })
  605. it('should send back a bad request status code', function (ctx) {
  606. expect(ctx.res.statusCode).to.equal(400)
  607. })
  608. })
  609. describe('with 0 length name', function () {
  610. beforeEach(function (ctx) {
  611. ctx.newName = ''
  612. ctx.req.body = { name: ctx.newName, source: ctx.source }
  613. ctx.EditorHttpController.renameEntity(ctx.req, ctx.res)
  614. })
  615. it('should send back a bad request status code', function (ctx) {
  616. expect(ctx.res.statusCode).to.equal(400)
  617. })
  618. })
  619. })
  620. describe('moveEntity', function () {
  621. beforeEach(async function (ctx) {
  622. await new Promise(resolve => {
  623. ctx.entityId = 'entity-id-123'
  624. ctx.entityType = 'entity-type'
  625. ctx.folderId = 'folder-id-123'
  626. ctx.req.params = {
  627. Project_id: ctx.project._id,
  628. entity_id: ctx.entityId,
  629. entity_type: ctx.entityType,
  630. }
  631. ctx.req.body = { folder_id: ctx.folderId, source: ctx.source }
  632. ctx.res.callback = resolve
  633. ctx.EditorHttpController.moveEntity(ctx.req, ctx.res)
  634. })
  635. })
  636. it('should call EditorController.moveEntity', function (ctx) {
  637. expect(ctx.EditorController.promises.moveEntity).to.have.been.calledWith(
  638. ctx.project._id,
  639. ctx.entityId,
  640. ctx.folderId,
  641. ctx.entityType,
  642. ctx.user._id,
  643. ctx.source
  644. )
  645. })
  646. it('should send back a success response', function (ctx) {
  647. expect(ctx.res.statusCode).to.equal(204)
  648. })
  649. })
  650. describe('deleteEntity', function () {
  651. beforeEach(async function (ctx) {
  652. await new Promise(resolve => {
  653. ctx.entityId = 'entity-id-123'
  654. ctx.entityType = 'entity-type'
  655. ctx.req.params = {
  656. Project_id: ctx.project._id,
  657. entity_id: ctx.entityId,
  658. entity_type: ctx.entityType,
  659. }
  660. ctx.res.callback = resolve
  661. ctx.EditorHttpController.deleteEntity(ctx.req, ctx.res)
  662. })
  663. })
  664. it('should call EditorController.deleteEntity', function (ctx) {
  665. expect(
  666. ctx.EditorController.promises.deleteEntity
  667. ).to.have.been.calledWith(
  668. ctx.project._id,
  669. ctx.entityId,
  670. ctx.entityType,
  671. 'editor',
  672. ctx.user._id
  673. )
  674. })
  675. it('should send back a success response', function (ctx) {
  676. expect(ctx.res.statusCode).to.equal(204)
  677. })
  678. })
  679. })