import { expect } from 'chai' import Settings from '@overleaf/settings' import RedisWrapper from '@overleaf/redis-wrapper' import { db, ObjectId } from '../../../../app/js/mongodb.js' import { fetchJson, fetchJsonWithResponse, fetchNothing, fetchStringWithResponse, RequestFailedError, } from '@overleaf/fetch-utils' const rclient = RedisWrapper.createClient(Settings.redis.project_history) const Keys = Settings.redis.project_history.key_schema export function resetDatabase(callback) { rclient.flushdb(callback) } export async function initializeProject(historyId) { const response = await fetchJsonWithResponse( 'http://127.0.0.1:3054/project', { method: 'POST', json: { historyId }, } ) expect(response.response.status).to.equal(200) return response.json.project } export async function flushProject(projectId, options = {}) { try { const response = await fetchNothing( `http://127.0.0.1:3054/project/${projectId}/flush`, { method: 'POST' } ) if (!options.allowErrors) { expect(response.status).to.equal(204) } return { statusCode: response.status } } catch (error) { if (options.allowErrors && error instanceof RequestFailedError) { return { statusCode: error.response.status } } throw error } } export async function getSummarizedUpdates(projectId, query) { const url = new URL(`http://127.0.0.1:3054/project/${projectId}/updates`) Object.keys(query).forEach(key => { url.searchParams.set(key, query[key]) }) return await fetchJson(url.toString()) } export async function getDiff(projectId, pathname, from, to) { const url = new URL(`http://127.0.0.1:3054/project/${projectId}/diff`) url.searchParams.set('pathname', pathname) url.searchParams.set('from', from) url.searchParams.set('to', to) return await fetchJson(url.toString()) } export async function getFileTreeDiff(projectId, from, to) { const url = new URL( `http://127.0.0.1:3054/project/${projectId}/filetree/diff` ) url.searchParams.set('from', from) url.searchParams.set('to', to) try { const { response, json } = await fetchJsonWithResponse(url.toString()) return { diff: json, statusCode: response.status } } catch (error) { if (error instanceof RequestFailedError) { return { diff: null, statusCode: error.response.status } } throw error } } export async function getChangesInChunkSince(projectId, since, options = {}) { const url = new URL( `http://127.0.0.1:3054/project/${projectId}/changes-in-chunk` ) url.searchParams.set('since', since) try { const { response, json } = await fetchJsonWithResponse(url.toString()) return { body: json, statusCode: response.status } } catch (error) { if (options.allowErrors && error instanceof RequestFailedError) { return { body: null, statusCode: error.response.status } } throw error } } export async function getLatestSnapshot(projectId) { return await fetchJson(`http://127.0.0.1:3054/project/${projectId}/snapshot`) } export async function getSnapshot(projectId, pathname, version, options = {}) { const url = `http://127.0.0.1:3054/project/${projectId}/version/${version}/${encodeURIComponent( pathname )}` try { const { response, body } = await fetchStringWithResponse(url) if (!options.allowErrors) { expect(response.status).to.equal(200) } return { body, statusCode: response.status } } catch (error) { if (options.allowErrors && error instanceof RequestFailedError) { return { body: null, statusCode: error.response.status } } throw error } } export async function pushRawUpdate(projectId, update) { await rclient.rpush( Keys.projectHistoryOps({ project_id: projectId }), JSON.stringify(update) ) } export async function setFirstOpTimestamp(projectId, timestamp) { await rclient.set( Keys.projectHistoryFirstOpTimestamp({ project_id: projectId }), timestamp ) } export async function getFirstOpTimestamp(projectId) { return await rclient.get( Keys.projectHistoryFirstOpTimestamp({ project_id: projectId }) ) } export async function clearFirstOpTimestamp(projectId) { await rclient.del( Keys.projectHistoryFirstOpTimestamp({ project_id: projectId }) ) } export function getQueueLength(projectId, callback) { rclient.llen(Keys.projectHistoryOps({ project_id: projectId }), callback) } export async function resyncHistory(projectId) { const response = await fetchNothing( `http://127.0.0.1:3054/project/${projectId}/resync`, { method: 'POST', json: { origin: { kind: 'test-origin' } }, } ) expect(response.status).to.equal(204) } export async function hardResyncHistory( projectId, { recoverCorruptedFiles } = {} ) { const params = new URLSearchParams({ force: 'true' }) if (recoverCorruptedFiles) { params.set('recoverCorruptedFiles', 'true') } const response = await fetchNothing( `http://127.0.0.1:3054/project/${projectId}/resync?${params}`, { method: 'POST', json: { origin: { kind: 'test-origin' } }, } ) expect(response.status).to.equal(204) } export async function createLabel( projectId, userId, version, comment, createdAt ) { return await fetchJson(`http://127.0.0.1:3054/project/${projectId}/labels`, { method: 'POST', json: { comment, version, created_at: createdAt, user_id: userId }, }) } export async function getLabels(projectId) { return await fetchJson(`http://127.0.0.1:3054/project/${projectId}/labels`) } export async function deleteLabelForUser(projectId, userId, labelId) { const response = await fetchNothing( `http://127.0.0.1:3054/project/${projectId}/user/${userId}/labels/${labelId}`, { method: 'DELETE' } ) expect(response.status).to.equal(204) } export async function deleteLabel(projectId, labelId) { const response = await fetchNothing( `http://127.0.0.1:3054/project/${projectId}/labels/${labelId}`, { method: 'DELETE' } ) expect(response.status).to.equal(204) } export async function setFailures(failureEntries) { await db.projectHistoryFailures.deleteMany({}) return await db.projectHistoryFailures.insertMany(failureEntries) } export function getFailure(projectId, callback) { db.projectHistoryFailures.findOne({ project_id: projectId }, callback) } export async function transferLabelOwnership(fromUser, toUser) { const response = await fetchNothing( `http://127.0.0.1:3054/user/${fromUser}/labels/transfer/${toUser}`, { method: 'POST' } ) expect(response.status).to.equal(204) } export async function getDump(projectId) { return await fetchJson(`http://127.0.0.1:3054/project/${projectId}/dump`) } export async function getFailures() { const { failures } = await fetchJson('http://127.0.0.1:3054/status/failures') return failures } export async function getSyncState(projectId) { return await db.projectHistorySyncState.findOne({ project_id: new ObjectId(projectId), }) } export async function getResyncPending(projectId) { return await fetchJson( `http://127.0.0.1:3054/project/${projectId}/resync-pending` ) } export async function injectStuckSyncState(projectId, docPaths) { await db.projectHistorySyncState.replaceOne( { project_id: new ObjectId(projectId) }, { project_id: new ObjectId(projectId), resyncProjectStructure: false, resyncDocContents: docPaths, stuckClearCount: 0, history: [], }, { upsert: true } ) } export async function deleteProject(projectId) { const response = await fetchNothing( `http://127.0.0.1:3054/project/${projectId}`, { method: 'DELETE' } ) expect(response.status).to.equal(204) }