Files
overleaf-cep/services/project-history/test/acceptance/js/FileTreeDiffTests.js
Domagoj Kriskovic 8f3d0677c3 promisify FileTreeDiffTests
GitOrigin-RevId: f7c9c119f28bdbf98c526d767ee06286c33ecfa2
2025-10-07 08:05:12 +00:00

784 lines
20 KiB
JavaScript

import { expect } from 'chai'
import crypto from 'node:crypto'
import mongodb from 'mongodb-legacy'
import nock from 'nock'
import * as ProjectHistoryClient from './helpers/ProjectHistoryClient.js'
import * as ProjectHistoryApp from './helpers/ProjectHistoryApp.js'
const { ObjectId } = mongodb
const MockHistoryStore = () => nock('http://127.0.0.1:3100')
const MockWeb = () => nock('http://127.0.0.1:3000')
const sha = data => crypto.createHash('sha1').update(data).digest('hex')
describe('FileTree Diffs', function () {
beforeEach(async function () {
await ProjectHistoryApp.promises.ensureRunning()
this.historyId = new ObjectId().toString()
this.projectId = new ObjectId().toString()
MockHistoryStore().post('/api/projects').reply(200, {
projectId: this.historyId,
})
MockWeb()
.get(`/project/${this.projectId}/details`)
.reply(200, {
name: 'Test Project',
overleaf: { history: { id: this.historyId } },
})
await ProjectHistoryClient.promises.initializeProject(this.historyId)
})
afterEach(function () {
return nock.cleanAll()
})
it('should return a diff of the updates to a doc from a single chunk', async function () {
MockHistoryStore()
.get(`/api/projects/${this.historyId}/versions/7/history`)
.reply(200, {
chunk: {
history: {
snapshot: {
files: {
'foo.tex': {
hash: sha('mock-sha-foo'),
stringLength: 42,
},
'renamed.tex': {
hash: sha('mock-sha-renamed'),
stringLength: 42,
},
'deleted.tex': {
hash: sha('mock-sha-deleted'),
stringLength: 42,
},
},
},
changes: [
{
operations: [
{
pathname: 'renamed.tex',
newPathname: 'newName.tex',
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
{
operations: [
{
pathname: 'foo.tex',
textOperation: ['lorem ipsum'],
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
{
operations: [
{
pathname: 'deleted.tex',
newPathname: '',
},
],
timestamp: '2017-12-04T10:29:22.905Z',
authors: [31],
},
{
operations: [
{
file: {
hash: sha('new-sha'),
stringLength: 42,
},
pathname: 'added.tex',
},
],
timestamp: '2017-12-04T10:29:22.905Z',
authors: [31],
},
],
},
startVersion: 3,
},
authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
})
const { diff } = await ProjectHistoryClient.getFileTreeDiff(
this.projectId,
3,
7
)
expect(diff).to.deep.equal({
diff: [
{
pathname: 'foo.tex',
operation: 'edited',
},
{
pathname: 'deleted.tex',
operation: 'removed',
deletedAtV: 5,
editable: true,
},
{
newPathname: 'newName.tex',
pathname: 'renamed.tex',
operation: 'renamed',
editable: true,
},
{
pathname: 'added.tex',
operation: 'added',
editable: true,
},
],
})
})
it('should return a diff of the updates to a doc across multiple chunks', async function () {
MockHistoryStore()
.get(`/api/projects/${this.historyId}/versions/5/history`)
.reply(200, {
chunk: {
history: {
snapshot: {
files: {
'foo.tex': {
// Updated in this chunk
hash: sha('mock-sha-foo'),
stringLength: 42,
},
'bar.tex': {
// Updated in the next chunk
hash: sha('mock-sha-bar'),
stringLength: 42,
},
'baz.tex': {
// Not updated
hash: sha('mock-sha-bar'),
stringLength: 42,
},
'renamed.tex': {
hash: sha('mock-sha-renamed'),
stringLength: 42,
},
'deleted.tex': {
hash: sha('mock-sha-deleted'),
stringLength: 42,
},
},
},
changes: [
{
operations: [
{
pathname: 'renamed.tex',
newPathname: 'newName.tex',
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
{
operations: [
{
pathname: 'foo.tex',
textOperation: ['lorem ipsum'],
},
],
timestamp: '2017-12-04T10:29:19.786Z',
authors: [31],
},
{
operations: [
{
pathname: 'deleted.tex',
newPathname: '',
},
],
timestamp: '2017-12-04T10:29:22.905Z',
authors: [31],
},
],
},
startVersion: 2,
},
authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
})
MockHistoryStore()
.get(`/api/projects/${this.historyId}/versions/7/history`)
.reply(200, {
chunk: {
history: {
snapshot: {
files: {
'foo.tex': {
hash: sha('mock-sha-foo'),
stringLength: 42,
},
'baz.tex': {
hash: sha('mock-sha-bar'),
stringLength: 42,
},
'newName.tex': {
hash: sha('mock-sha-renamed'),
stringLength: 42,
},
},
},
changes: [
{
operations: [
{
file: {
hash: sha('new-sha'),
stringLength: 42,
},
pathname: 'added.tex',
},
],
timestamp: '2017-12-04T10:29:22.905Z',
authors: [31],
},
{
operations: [
{
pathname: 'bar.tex',
textOperation: ['lorem ipsum'],
},
],
timestamp: '2017-12-04T10:29:23.786Z',
authors: [31],
},
],
},
startVersion: 5,
},
authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
})
const { diff } = await ProjectHistoryClient.getFileTreeDiff(
this.projectId,
2,
7
)
expect(diff).to.deep.equal({
diff: [
{
pathname: 'foo.tex',
operation: 'edited',
},
{
pathname: 'bar.tex',
operation: 'edited',
},
{
pathname: 'baz.tex',
editable: true,
},
{
pathname: 'deleted.tex',
operation: 'removed',
deletedAtV: 4,
editable: true,
},
{
newPathname: 'newName.tex',
pathname: 'renamed.tex',
operation: 'renamed',
editable: true,
},
{
pathname: 'added.tex',
operation: 'added',
editable: true,
},
],
})
})
it('should return a diff that includes multiple renames', async function () {
MockHistoryStore()
.get(`/api/projects/${this.historyId}/versions/5/history`)
.reply(200, {
chunk: {
history: {
snapshot: {
files: {
'one.tex': {
hash: sha('mock-sha'),
stringLength: 42,
},
},
},
changes: [
{
operations: [
{
pathname: 'one.tex',
newPathname: 'two.tex',
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
{
operations: [
{
pathname: 'two.tex',
newPathname: 'three.tex',
},
],
timestamp: '2017-12-04T10:29:22.905Z',
authors: [31],
},
],
},
startVersion: 3,
},
authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
})
const { diff } = await ProjectHistoryClient.getFileTreeDiff(
this.projectId,
3,
5
)
expect(diff).to.deep.equal({
diff: [
{
newPathname: 'three.tex',
pathname: 'one.tex',
operation: 'renamed',
editable: true,
},
],
})
})
it('should handle deleting then re-adding a file', async function () {
MockHistoryStore()
.get(`/api/projects/${this.historyId}/versions/5/history`)
.reply(200, {
chunk: {
history: {
snapshot: {
files: {
'one.tex': {
hash: sha('mock-sha'),
stringLength: 42,
},
},
},
changes: [
{
operations: [
{
pathname: 'one.tex',
newPathname: '',
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
{
operations: [
{
pathname: 'one.tex',
file: {
hash: sha('mock-sha'),
},
},
],
timestamp: '2017-12-04T10:29:22.905Z',
authors: [31],
},
],
},
startVersion: 3,
},
authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
})
const { diff } = await ProjectHistoryClient.getFileTreeDiff(
this.projectId,
3,
5
)
expect(diff).to.deep.equal({
diff: [
{
pathname: 'one.tex',
operation: 'added',
editable: null,
},
],
})
})
it('should handle deleting the renaming a file to the same place', async function () {
MockHistoryStore()
.get(`/api/projects/${this.historyId}/versions/5/history`)
.reply(200, {
chunk: {
history: {
snapshot: {
files: {
'one.tex': {
hash: sha('mock-sha-one'),
stringLength: 42,
},
'two.tex': {
hash: sha('mock-sha-two'),
stringLength: 42,
},
},
},
changes: [
{
operations: [
{
pathname: 'one.tex',
newPathname: '',
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
{
operations: [
{
pathname: 'two.tex',
newPathname: 'one.tex',
},
],
timestamp: '2017-12-04T10:29:22.905Z',
authors: [31],
},
],
},
startVersion: 3,
},
authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
})
const { diff } = await ProjectHistoryClient.getFileTreeDiff(
this.projectId,
3,
5
)
expect(diff).to.deep.equal({
diff: [
{
pathname: 'two.tex',
newPathname: 'one.tex',
operation: 'renamed',
editable: true,
},
],
})
})
it('should handle adding then renaming a file', async function () {
MockHistoryStore()
.get(`/api/projects/${this.historyId}/versions/5/history`)
.reply(200, {
chunk: {
history: {
snapshot: {
files: {},
},
changes: [
{
operations: [
{
pathname: 'one.tex',
file: {
hash: sha('mock-sha'),
stringLength: 42,
},
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
{
operations: [
{
pathname: 'one.tex',
newPathname: 'two.tex',
},
],
timestamp: '2017-12-04T10:29:22.905Z',
authors: [31],
},
],
},
startVersion: 3,
},
authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
})
const { diff } = await ProjectHistoryClient.getFileTreeDiff(
this.projectId,
3,
5
)
expect(diff).to.deep.equal({
diff: [
{
pathname: 'two.tex',
operation: 'added',
editable: true,
},
],
})
})
it('should return 422 with a chunk with an invalid rename', async function () {
MockHistoryStore()
.get(`/api/projects/${this.historyId}/versions/6/history`)
.reply(200, {
chunk: {
history: {
snapshot: {
files: {
'foo.tex': {
hash: sha('mock-sha-foo'),
stringLength: 42,
},
'bar.tex': {
hash: sha('mock-sha-bar'),
stringLength: 42,
},
},
},
changes: [
{
operations: [
{
pathname: 'foo.tex',
newPathname: 'bar.tex',
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
],
},
startVersion: 5,
},
authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
})
const { statusCode } = await ProjectHistoryClient.getFileTreeDiff(
this.projectId,
5,
6
)
expect(statusCode).to.equal(422)
})
it('should return 200 with a chunk with an invalid add', async function () {
MockHistoryStore()
.get(`/api/projects/${this.historyId}/versions/6/history`)
.reply(200, {
chunk: {
history: {
snapshot: {
files: {
'foo.tex': {
hash: sha('mock-sha-foo'),
stringLength: 42,
},
},
},
changes: [
{
operations: [
{
file: {
hash: sha('new-sha'),
},
pathname: 'foo.tex',
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
],
},
startVersion: 5,
},
authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
})
const { diff, statusCode } = await ProjectHistoryClient.getFileTreeDiff(
this.projectId,
5,
6
)
expect(diff).to.deep.equal({
diff: [
{
pathname: 'foo.tex',
operation: 'added',
editable: null,
},
],
})
expect(statusCode).to.equal(200)
})
it('should handle edits of missing/invalid files ', async function () {
MockHistoryStore()
.get(`/api/projects/${this.historyId}/versions/5/history`)
.reply(200, {
chunk: {
history: {
snapshot: {
files: {},
},
changes: [
{
operations: [
{
pathname: 'new.tex',
textOperation: ['lorem ipsum'],
},
],
timestamp: '2017-12-04T10:29:18.786Z',
authors: [31],
},
{
operations: [
{
pathname: '',
textOperation: ['lorem ipsum'],
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
],
},
startVersion: 3,
},
authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
})
const { diff } = await ProjectHistoryClient.getFileTreeDiff(
this.projectId,
3,
5
)
expect(diff).to.deep.equal({
diff: [
{
operation: 'edited',
pathname: 'new.tex',
},
],
})
})
it('should handle deletions of missing/invalid files ', async function () {
MockHistoryStore()
.get(`/api/projects/${this.historyId}/versions/5/history`)
.reply(200, {
chunk: {
history: {
snapshot: {
files: {},
},
changes: [
{
operations: [
{
pathname: 'missing.tex',
newPathname: '',
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
{
operations: [
{
pathname: '',
newPathname: '',
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
],
},
startVersion: 3,
},
authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
})
const { diff } = await ProjectHistoryClient.getFileTreeDiff(
this.projectId,
3,
5
)
expect(diff).to.deep.equal({
diff: [],
})
})
return it('should handle renames of missing/invalid files ', async function () {
MockHistoryStore()
.get(`/api/projects/${this.historyId}/versions/5/history`)
.reply(200, {
chunk: {
history: {
snapshot: {
files: {},
},
changes: [
{
operations: [
{
pathname: 'missing.tex',
newPathname: 'missing-renamed.tex',
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
{
operations: [
{
pathname: '',
newPathname: 'missing-renamed-other.tex',
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
],
},
startVersion: 3,
},
authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
})
const { diff } = await ProjectHistoryClient.getFileTreeDiff(
this.projectId,
3,
5
)
expect(diff).to.deep.equal({
diff: [],
})
})
})