DeletionTests.mjs 16 KB

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