DeletionTests.mjs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. import logger from '@overleaf/logger'
  2. import sinon from 'sinon'
  3. import User from './helpers/User.mjs'
  4. import Subscription from './helpers/Subscription.mjs'
  5. import request from './helpers/request.js'
  6. import async from 'async'
  7. import { expect } from 'chai'
  8. import settings from '@overleaf/settings'
  9. import { db, ObjectId } from '../../../app/src/infrastructure/mongodb.js'
  10. import Features from '../../../app/src/infrastructure/Features.js'
  11. import MockDocstoreApiClass from './mocks/MockDocstoreApi.mjs'
  12. import MockFilestoreApiClass from './mocks/MockFilestoreApi.mjs'
  13. import MockChatApiClass from './mocks/MockChatApi.mjs'
  14. import MockGitBridgeApiClass from './mocks/MockGitBridgeApi.mjs'
  15. import MockHistoryBackupDeletionApiClass from './mocks/MockHistoryBackupDeletionApi.mjs'
  16. let MockDocstoreApi,
  17. MockFilestoreApi,
  18. MockChatApi,
  19. MockGitBridgeApi,
  20. MockHistoryBackupDeletionApi
  21. let spy
  22. before(function () {
  23. MockDocstoreApi = MockDocstoreApiClass.instance()
  24. MockFilestoreApi = MockFilestoreApiClass.instance()
  25. MockChatApi = MockChatApiClass.instance()
  26. MockGitBridgeApi = MockGitBridgeApiClass.instance()
  27. MockHistoryBackupDeletionApi = MockHistoryBackupDeletionApiClass.instance()
  28. })
  29. describe('Deleting a user', function () {
  30. beforeEach(function (done) {
  31. spy = sinon.spy(logger, 'info')
  32. async.auto(
  33. {
  34. user: cb => {
  35. const user = new User()
  36. user.ensureUserExists(() => {
  37. cb(null, user)
  38. })
  39. },
  40. login: [
  41. 'user',
  42. (results, cb) => {
  43. results.user.login(cb)
  44. },
  45. ],
  46. subscription: [
  47. 'user',
  48. 'login',
  49. (results, cb) => {
  50. const subscription = new Subscription({
  51. admin_id: results.user._id,
  52. })
  53. subscription.ensureExists(err => {
  54. cb(err, subscription)
  55. })
  56. },
  57. ],
  58. },
  59. (err, results) => {
  60. expect(err).not.to.exist
  61. this.user = results.user
  62. this.subscription = results.subscription
  63. done()
  64. }
  65. )
  66. })
  67. afterEach(function () {
  68. spy.restore()
  69. })
  70. it('Should remove the user from active users', function (done) {
  71. this.user.get((error, user) => {
  72. expect(error).not.to.exist
  73. expect(user).to.exist
  74. this.user.deleteUser(error => {
  75. expect(error).not.to.exist
  76. this.user.get((error, user) => {
  77. expect(error).not.to.exist
  78. expect(user).not.to.exist
  79. done()
  80. })
  81. })
  82. })
  83. })
  84. it('Should create a soft-deleted user', function (done) {
  85. this.user.get((error, user) => {
  86. expect(error).not.to.exist
  87. this.user.deleteUser(error => {
  88. expect(error).not.to.exist
  89. db.deletedUsers.findOne(
  90. { 'user._id': user._id },
  91. (error, deletedUser) => {
  92. expect(error).not.to.exist
  93. expect(deletedUser).to.exist
  94. // it should set the 'deleterData' correctly
  95. expect(deletedUser.deleterData.deleterId.toString()).to.equal(
  96. user._id.toString()
  97. )
  98. expect(deletedUser.deleterData.deletedUserId.toString()).to.equal(
  99. user._id.toString()
  100. )
  101. expect(deletedUser.deleterData.deletedUserReferralId).to.equal(
  102. user.referal_id
  103. )
  104. // it should set the 'user' correctly
  105. expect(deletedUser.user._id.toString()).to.equal(
  106. user._id.toString()
  107. )
  108. expect(deletedUser.user.email).to.equal(user.email)
  109. done()
  110. }
  111. )
  112. })
  113. })
  114. })
  115. it("Should delete the user's projects", function (done) {
  116. this.user.createProject('wombat', (error, projectId) => {
  117. expect(error).not.to.exist
  118. this.user.getProject(projectId, (error, project) => {
  119. expect(error).not.to.exist
  120. expect(project).to.exist
  121. this.user.deleteUser(error => {
  122. expect(error).not.to.exist
  123. this.user.getProject(projectId, (error, project) => {
  124. expect(error).not.to.exist
  125. expect(project).not.to.exist
  126. done()
  127. })
  128. })
  129. })
  130. })
  131. })
  132. describe('when scrubbing the user', function () {
  133. beforeEach(function (done) {
  134. this.user.get((error, user) => {
  135. if (error) {
  136. throw error
  137. }
  138. this.userId = user._id
  139. this.user.deleteUser(done)
  140. })
  141. })
  142. it('Should remove the user data from mongo', function (done) {
  143. db.deletedUsers.findOne(
  144. { 'deleterData.deletedUserId': this.userId },
  145. (error, deletedUser) => {
  146. expect(error).not.to.exist
  147. expect(deletedUser).to.exist
  148. expect(deletedUser.deleterData.deleterIpAddress).to.exist
  149. expect(deletedUser.user).to.exist
  150. request.post(
  151. `/internal/users/${this.userId}/expire`,
  152. {
  153. auth: {
  154. user: settings.apis.web.user,
  155. pass: settings.apis.web.pass,
  156. sendImmediately: true,
  157. },
  158. },
  159. (error, res) => {
  160. expect(error).not.to.exist
  161. expect(res.statusCode).to.equal(204)
  162. db.deletedUsers.findOne(
  163. { 'deleterData.deletedUserId': this.userId },
  164. (error, deletedUser) => {
  165. expect(error).not.to.exist
  166. expect(deletedUser).to.exist
  167. expect(deletedUser.deleterData.deleterIpAddress).not.to.exist
  168. expect(deletedUser.user).not.to.exist
  169. done()
  170. }
  171. )
  172. }
  173. )
  174. }
  175. )
  176. })
  177. })
  178. })
  179. describe('Deleting a project', function () {
  180. beforeEach(function (done) {
  181. spy = sinon.spy(logger, 'info')
  182. this.user = new User()
  183. this.projectName = 'wombat'
  184. this.user.ensureUserExists(() => {
  185. this.user.login(() => {
  186. this.user.createProject(this.projectName, (_e, projectId) => {
  187. this.projectId = projectId
  188. done()
  189. })
  190. })
  191. })
  192. })
  193. afterEach(function () {
  194. logger.info.restore()
  195. })
  196. it('Should remove the project from active projects', function (done) {
  197. this.user.getProject(this.projectId, (error, project) => {
  198. expect(error).not.to.exist
  199. expect(project).to.exist
  200. this.user.deleteProject(this.projectId, error => {
  201. expect(error).not.to.exist
  202. this.user.getProject(this.projectId, (error, project) => {
  203. expect(error).not.to.exist
  204. expect(project).not.to.exist
  205. done()
  206. })
  207. })
  208. })
  209. })
  210. it('Should create a soft-deleted project', function (done) {
  211. this.user.getProject(this.projectId, (error, project) => {
  212. expect(error).not.to.exist
  213. this.user.get((error, user) => {
  214. expect(error).not.to.exist
  215. this.user.deleteProject(this.projectId, error => {
  216. expect(error).not.to.exist
  217. db.deletedProjects.findOne(
  218. { 'deleterData.deletedProjectId': project._id },
  219. (error, deletedProject) => {
  220. expect(error).not.to.exist
  221. expect(deletedProject).to.exist
  222. // it should set the 'deleterData' correctly
  223. expect(deletedProject.deleterData.deleterId.toString()).to.equal(
  224. user._id.toString()
  225. )
  226. expect(
  227. deletedProject.deleterData.deletedProjectId.toString()
  228. ).to.equal(project._id.toString())
  229. expect(
  230. deletedProject.deleterData.deletedProjectOwnerId.toString()
  231. ).to.equal(user._id.toString())
  232. // it should set the 'user' correctly
  233. expect(deletedProject.project._id.toString()).to.equal(
  234. project._id.toString()
  235. )
  236. expect(deletedProject.project.name).to.equal(this.projectName)
  237. done()
  238. }
  239. )
  240. })
  241. })
  242. })
  243. })
  244. describe('When the project has docs', function () {
  245. beforeEach(function (done) {
  246. this.user.getProject(this.projectId, (error, project) => {
  247. if (error) {
  248. throw error
  249. }
  250. this.user.createDocInProject(
  251. this.projectId,
  252. project.rootFolder[0]._id,
  253. 'potato',
  254. (error, docId) => {
  255. if (error) {
  256. throw error
  257. }
  258. this.docId = docId
  259. done()
  260. }
  261. )
  262. MockFilestoreApi.files[this.projectId.toString()] = {
  263. dummyFile: 'wombat',
  264. }
  265. MockChatApi.projects[this.projectId.toString()] = ['message']
  266. if (Features.hasFeature('git-bridge')) {
  267. MockGitBridgeApi.projects[this.projectId.toString()] = {
  268. data: 'some-data',
  269. }
  270. }
  271. })
  272. })
  273. describe('When the deleted project is expired', function () {
  274. beforeEach(function (done) {
  275. this.user.deleteProject(this.projectId, error => {
  276. if (error) {
  277. throw error
  278. }
  279. done()
  280. })
  281. })
  282. it('Should log a successful deletion', function (done) {
  283. request.post(
  284. `/internal/project/${this.projectId}/expire-deleted-project`,
  285. {
  286. auth: {
  287. user: settings.apis.web.user,
  288. pass: settings.apis.web.pass,
  289. sendImmediately: true,
  290. },
  291. },
  292. (error, res) => {
  293. expect(error).not.to.exist
  294. expect(res.statusCode).to.equal(200)
  295. expect(spy).to.have.been.calledWithMatch(
  296. { projectId: this.projectId, userId: this.user._id },
  297. 'expired deleted project successfully'
  298. )
  299. done()
  300. }
  301. )
  302. })
  303. it('Should destroy the docs', function (done) {
  304. expect(
  305. MockDocstoreApi.docs[this.projectId.toString()][this.docId.toString()]
  306. ).to.exist
  307. request.post(
  308. `/internal/project/${this.projectId}/expire-deleted-project`,
  309. {
  310. auth: {
  311. user: settings.apis.web.user,
  312. pass: settings.apis.web.pass,
  313. sendImmediately: true,
  314. },
  315. },
  316. (error, res) => {
  317. expect(error).not.to.exist
  318. expect(res.statusCode).to.equal(200)
  319. expect(MockDocstoreApi.docs[this.projectId.toString()]).not.to.exist
  320. done()
  321. }
  322. )
  323. })
  324. it('Should destroy the files if filestore is in use', function (done) {
  325. expect(MockFilestoreApi.files[this.projectId.toString()]).to.exist
  326. request.post(
  327. `/internal/project/${this.projectId}/expire-deleted-project`,
  328. {
  329. auth: {
  330. user: settings.apis.web.user,
  331. pass: settings.apis.web.pass,
  332. sendImmediately: true,
  333. },
  334. },
  335. (error, res) => {
  336. expect(error).not.to.exist
  337. expect(res.statusCode).to.equal(200)
  338. if (Features.hasFeature('filestore')) {
  339. expect(MockFilestoreApi.files[this.projectId.toString()]).not.to
  340. .exist
  341. } else {
  342. // don't touch files in filestore if it's not in use
  343. expect(MockFilestoreApi.files[this.projectId.toString()]).to.exist
  344. }
  345. done()
  346. }
  347. )
  348. })
  349. it('Should destroy the chat', function (done) {
  350. expect(MockChatApi.projects[this.projectId.toString()]).to.exist
  351. request.post(
  352. `/internal/project/${this.projectId}/expire-deleted-project`,
  353. {
  354. auth: {
  355. user: settings.apis.web.user,
  356. pass: settings.apis.web.pass,
  357. sendImmediately: true,
  358. },
  359. },
  360. (error, res) => {
  361. expect(error).not.to.exist
  362. expect(res.statusCode).to.equal(200)
  363. expect(MockChatApi.projects[this.projectId.toString()]).not.to.exist
  364. done()
  365. }
  366. )
  367. })
  368. it('Should remove the project data from mongo', function (done) {
  369. db.deletedProjects.findOne(
  370. { 'deleterData.deletedProjectId': new ObjectId(this.projectId) },
  371. (error, deletedProject) => {
  372. expect(error).not.to.exist
  373. expect(deletedProject).to.exist
  374. expect(deletedProject.project).to.exist
  375. expect(deletedProject.deleterData.deleterIpAddress).to.exist
  376. expect(deletedProject.deleterData.deletedAt).to.exist
  377. request.post(
  378. `/internal/project/${this.projectId}/expire-deleted-project`,
  379. {
  380. auth: {
  381. user: settings.apis.web.user,
  382. pass: settings.apis.web.pass,
  383. sendImmediately: true,
  384. },
  385. },
  386. (error, res) => {
  387. expect(error).not.to.exist
  388. expect(res.statusCode).to.equal(200)
  389. db.deletedProjects.findOne(
  390. {
  391. 'deleterData.deletedProjectId': new ObjectId(
  392. this.projectId
  393. ),
  394. },
  395. (error, deletedProject) => {
  396. expect(error).not.to.exist
  397. expect(deletedProject).to.exist
  398. expect(deletedProject.project).not.to.exist
  399. expect(deletedProject.deleterData.deleterIpAddress).not.to
  400. .exist
  401. expect(deletedProject.deleterData.deletedAt).to.exist
  402. done()
  403. }
  404. )
  405. }
  406. )
  407. }
  408. )
  409. })
  410. if (Features.hasFeature('saas')) {
  411. it('Should destroy the history backup', function (done) {
  412. MockHistoryBackupDeletionApi.prepareProject(this.projectId, 204)
  413. request.post(
  414. `/internal/project/${this.projectId}/expire-deleted-project`,
  415. {
  416. auth: {
  417. user: settings.apis.web.user,
  418. pass: settings.apis.web.pass,
  419. sendImmediately: true,
  420. },
  421. },
  422. (error, res) => {
  423. expect(error).not.to.exist
  424. expect(res.statusCode).to.equal(200)
  425. expect(
  426. MockHistoryBackupDeletionApi.projects[this.projectId.toString()]
  427. ).not.to.exist
  428. done()
  429. }
  430. )
  431. })
  432. it('Should abort when the history backup cannot be deleted', function (done) {
  433. MockHistoryBackupDeletionApi.prepareProject(this.projectId, 422)
  434. request.post(
  435. `/internal/project/${this.projectId}/expire-deleted-project`,
  436. {
  437. auth: {
  438. user: settings.apis.web.user,
  439. pass: settings.apis.web.pass,
  440. sendImmediately: true,
  441. },
  442. },
  443. (error, res) => {
  444. expect(error).not.to.exist
  445. expect(res.statusCode).to.equal(500)
  446. expect(
  447. MockHistoryBackupDeletionApi.projects[this.projectId.toString()]
  448. ).to.exist
  449. db.deletedProjects.findOne(
  450. {
  451. 'deleterData.deletedProjectId': new ObjectId(this.projectId),
  452. },
  453. (error, deletedProject) => {
  454. expect(error).not.to.exist
  455. expect(deletedProject).to.exist
  456. expect(deletedProject.project).to.exist
  457. done()
  458. }
  459. )
  460. }
  461. )
  462. })
  463. }
  464. })
  465. })
  466. if (Features.hasFeature('git-bridge')) {
  467. describe('When the project has git-bridge data', function () {
  468. beforeEach(function () {
  469. MockGitBridgeApi.projects[this.projectId.toString()] = {
  470. data: 'some-data',
  471. }
  472. })
  473. describe('When the deleted project is expired', function () {
  474. beforeEach(function (done) {
  475. this.user.deleteProject(this.projectId, error => {
  476. if (error) {
  477. return done(error)
  478. }
  479. request.post(
  480. `/internal/project/${this.projectId}/expire-deleted-project`,
  481. {
  482. auth: {
  483. user: settings.apis.web.user,
  484. pass: settings.apis.web.pass,
  485. sendImmediately: true,
  486. },
  487. },
  488. (error, res) => {
  489. if (error) {
  490. return done(error)
  491. }
  492. expect(res.statusCode).to.equal(200)
  493. done()
  494. }
  495. )
  496. })
  497. })
  498. it('should delete the git-bridge data', function () {
  499. expect(MockGitBridgeApi.projects[this.projectId.toString()]).not.to
  500. .exist
  501. })
  502. })
  503. })
  504. }
  505. })