From 9de241ca940739a556f446b3d8a3311809417039 Mon Sep 17 00:00:00 2001 From: Bogdan Cornea Date: Mon, 23 Dec 2024 11:30:34 +0000 Subject: [PATCH 01/15] first commit - create endpoints/functions/tests --- .../projects/models/commons/modelList.js | 55 +++- .../teamspaces/projects/models/containers.js | 8 +- .../teamspaces/projects/models/federations.js | 22 +- .../projects/models/common/revisions.js | 292 ++++++++++++------ .../projects/models/federations/revisions.js | 103 ++++++ .../projects/models/common/revisions.test.js | 64 ++++ .../models/federations/revisions.test.js | 44 +++ .../projects/models/containers.test.js | 57 +++- .../projects/models/federations.test.js | 103 ++++++ 9 files changed, 638 insertions(+), 110 deletions(-) diff --git a/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js b/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js index f04afa19097..749392c97d1 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js +++ b/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js @@ -18,9 +18,16 @@ const { addModelToProject, getProjectById, removeModelFromProject } = require('../../../../../models/projectSettings'); const { hasProjectAdminPermissions, isTeamspaceAdmin } = require('../../../../../utils/permissions/permissions'); const { USERS_DB_NAME } = require('../../../../../models/users.constants'); -const { addModel } = require('../../../../../models/modelSettings'); +const { addModel, getContainers } = require('../../../../../models/modelSettings'); +const { modelTypes } = require('../../../../../models/modelSettings.constants'); const { getFavourites } = require('../../../../../models/users'); const { removeModelData } = require('../../../../../utils/helper/models'); +const UUIDParse = require('uuid-parse'); +const CryptoJs = require('crypto-js'); +const { getFile } = require('../../../../../services/filesManager') +const { getLatestRevision, getRevisionByIdOrTag } = require('../../../../../models/revisions') +const { getRefEntry } = require('../../../../../models/fileRefs') + const ModelList = {}; @@ -50,11 +57,53 @@ ModelList.getModelList = async (teamspace, project, user, modelSettings) => { return modelSettings.flatMap(({ _id, name, permissions: modelPerms }) => { const perm = modelPerms ? modelPerms.find((entry) => entry.user === user) : undefined; return (!isAdmin && !perm) - ? [] : { _id, + ? [] : { + _id, name, role: isAdmin ? USERS_DB_NAME : perm.permission, - isFavourite: favourites.includes(_id) }; + isFavourite: favourites.includes(_id) + }; }); }; +ModelList.getModelMD5Hash = async (teamspace, container, revision, user) => { + const [isAdmin, containers] = await Promise.all([ + isTeamspaceAdmin(teamspace, user), + getContainers(teamspace, [container], { _id: 1, name: 1, permissions: 1 }) + ]) + let rev; + + //if not allowed just return nothing + if (!isAdmin && !containers[0].permissions?.some(permission => permission?.user === user)) return; + + //retrieve the right revision + if (revision?.length) { + rev = await getRevisionByIdOrTag(teamspace, container, modelTypes.CONTAINER, revision, { rFile: 1, timestamp: 1, fileSize: 1 }, { includeVoid: false }) + } else { + rev = await getLatestRevision(teamspace, container, modelTypes.CONTAINER, { rFile: 1, timestamp: 1, fileSize: 1 }) + } + + //check if anything is in there + if (!rev.rFile?.length) return; + + const filename = rev.rFile[0]; + + const file = await getFile(teamspace, `${container}.history`, filename); + const refEntry = await getRefEntry(teamspace, `${container}.history.ref`, filename); + + const hash = CryptoJs.MD5(file).toString(); + const code = UUIDParse.unparse(rev._id.buffer); + const uploadedAt = new Date(rev.timestamp).getTime(); + + return { + container, + code, + uploadedAt, + hash, + filename, + size: refEntry.size + }; + +} + module.exports = ModelList; diff --git a/backend/src/v5/processors/teamspaces/projects/models/containers.js b/backend/src/v5/processors/teamspaces/projects/models/containers.js index f822d5fec35..bc7d1e6dced 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/containers.js +++ b/backend/src/v5/processors/teamspaces/projects/models/containers.js @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -const { addModel, deleteModel, getModelList } = require('./commons/modelList'); +const { addModel, deleteModel, getModelList, getModelMD5Hash } = require('./commons/modelList'); const { appendFavourites, deleteFavourites } = require('./commons/favourites'); const { getContainerById, getContainers, updateModelSettings } = require('../../../../models/modelSettings'); const { getLatestRevision, getRevisionByIdOrTag, getRevisionCount, getRevisionFormat, getRevisions, updateRevisionStatus } = require('../../../../models/revisions'); @@ -34,6 +34,7 @@ const { queueModelUpload } = require('../../../../services/modelProcessing'); const { templates } = require('../../../../utils/responseCodes'); const { timestampToString } = require('../../../../utils/helper/dates'); + const Containers = { ...Groups, ...Views, ...Tickets, ...Comments, ...TicketGroups }; Containers.addContainer = addModel; @@ -133,4 +134,9 @@ Containers.updateSettings = updateModelSettings; Containers.getSettings = (teamspace, container) => getContainerById(teamspace, container, { corID: 0, account: 0, permissions: 0 }); +Containers.getRevisionMD5Hash = async (teamspace, container, revision, user) =>{ + const response = await getModelMD5Hash(teamspace, container, revision, user) + return response; +} + module.exports = Containers; diff --git a/backend/src/v5/processors/teamspaces/projects/models/federations.js b/backend/src/v5/processors/teamspaces/projects/models/federations.js index 6e1e61ef2fb..fdae212da0a 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/federations.js +++ b/backend/src/v5/processors/teamspaces/projects/models/federations.js @@ -17,7 +17,7 @@ const { addModel, deleteModel, getModelList } = require('./commons/modelList'); const { appendFavourites, deleteFavourites } = require('./commons/favourites'); -const { getFederationById, getFederations, updateModelSettings } = require('../../../../models/modelSettings'); +const { getContainers, getFederationById, getFederations, updateModelSettings } = require('../../../../models/modelSettings'); const Comments = require('./commons/tickets.comments'); const Groups = require('./commons/groups'); const TicketGroups = require('./commons/tickets.groups'); @@ -28,6 +28,7 @@ const { getOpenTicketsCount } = require('./commons/tickets'); const { getProjectById } = require('../../../../models/projectSettings'); const { modelTypes } = require('../../../../models/modelSettings.constants'); const { queueFederationUpdate } = require('../../../../services/modelProcessing'); +const { getModelMD5Hash } = require('../../projects/models/commons/modelList') const Federations = { ...Groups, ...Views, ...Tickets, ...Comments, ...TicketGroups }; @@ -75,7 +76,7 @@ const getLastUpdatesFromModels = async (teamspace, models) => { } return lastUpdates.length ? lastUpdates.sort((a, b) => b.timestamp - - a.timestamp)[0].timestamp : undefined; + - a.timestamp)[0].timestamp : undefined; }; Federations.getFederationStats = async (teamspace, project, federation) => { @@ -107,4 +108,21 @@ Federations.updateSettings = updateModelSettings; Federations.getSettings = (teamspace, federation) => getFederationById(teamspace, federation, { corID: 0, account: 0, permissions: 0, subModels: 0, federate: 0 }); +Federations.getMD5Hash = async (teamspace, federation, user) => { + const { subModels: containers } = await getFederationById(teamspace, federation, { subModels: 1 }); + const containerWithMetadata = await getContainers(teamspace, containers.map(container => container._id), { _id: 1, name: 1, permissions: 1 }) + + const listOfPromises = containerWithMetadata.map(container => { + return getModelMD5Hash(teamspace, container._id, null, user) + }) + + const promiseResponses = await Promise.allSettled(listOfPromises) + const responses = promiseResponses + .filter(response => response.status === 'fulfilled' && response.value !== undefined) + .map(response => response.value) + + return responses + +} + module.exports = Federations; diff --git a/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js b/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js index 299f0162ddf..41e56e252d6 100644 --- a/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js +++ b/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js @@ -59,6 +59,19 @@ const getRevisions = (modelType) => async (req, res, next) => { } }; +const getRevisionMD5Hash = () => async (req, res) => { + const { teamspace, container, revision } = req.params + const user = getUserFromSession(req.session) + + try { + const response = await Containers.getRevisionMD5Hash(teamspace, container, revision, user) + respond(req, res, templates.ok, response) + } catch (err) { + /* istanbul ignore next */ + respond(req, res, err) + } +} + const updateRevisionStatus = (modelType) => async (req, res) => { const { teamspace, project, model, revision } = req.params; const status = req.body.void; @@ -155,7 +168,7 @@ const establishRoutes = (modelType) => { * type: string * format: uuid * - name: type - * description: Model type + * description: Model type * in: path * required: true * schema: @@ -228,13 +241,13 @@ const establishRoutes = (modelType) => { * description: Whether revision is void or not * example: false * examples: - * containers: + * containers: * summary: containers - * value: + * value: * revisions: [{ _id: ef0855b6-4cc7-4be1-b2d6-c032dce7806a, author: someUser, timestamp: 1644925152000, format: .rvt, tag: rev01, desc: The Architecture model of the Lego House, void: true }] * drawings: * summary: drawings - * value: + * value: * revisions: [{ _id: ef0855b6-4cc7-4be1-b2d6-c032dce7806a, author: someUser, timestamp: 1644925152000, format: .rvt, statusCode: S0, revCode: P01, desc: The Architecture model of the Lego House, void: true }] */ router.get('', hasReadAccessToModel[modelType], getRevisions(modelType), serialiseRevisionArray); @@ -261,7 +274,7 @@ const establishRoutes = (modelType) => { * type: string * format: uuid * - name: type - * description: Model type + * description: Model type * in: path * required: true * schema: @@ -282,7 +295,7 @@ const establishRoutes = (modelType) => { * properties: * tag: * description: Unique revision name - * type: string + * type: string * example: rev01 * statusCode: * type: string @@ -347,7 +360,7 @@ const establishRoutes = (modelType) => { * type: string * format: uuid * - name: type - * description: Model type + * description: Model type * in: path * required: true * schema: @@ -368,7 +381,7 @@ const establishRoutes = (modelType) => { * type: string * requestBody: * content: - * application/json: + * application/json: * schema: * properties: * void: @@ -387,104 +400,177 @@ const establishRoutes = (modelType) => { router.patch('/:revision', hasWriteAccessToModel[modelType], validateUpdateRevisionData, updateRevisionStatus(modelType)); if (modelType === modelTypes.CONTAINER) { - /** - * @openapi - * /teamspaces/{teamspace}/projects/{project}/containers/{container}/revisions/{revision}/files: - * get: - * description: Downloads the container files of the selected revision - * tags: [Revisions] - * operationId: downloadContainerRevisionFiles - * parameters: - * - name: teamspace - * description: Name of teamspace - * in: path - * required: true - * schema: - * type: string - * - name: project - * description: Project ID - * in: path - * required: true - * schema: - * type: string - * format: uuid - * - name: container - * description: Container ID - * in: path - * required: true - * schema: - * type: string - * format: uuid - * - name: revision - * description: Revision ID or Revision tag - * in: path - * required: true - * schema: - * type: string - * responses: - * 401: - * $ref: "#/components/responses/notLoggedIn" - * 404: - * $ref: "#/components/responses/teamspaceNotFound" - * 200: - * description: downloads the revision files - * content: - * application/octet-stream: - * schema: - * type: string - * format: binary - */ + /** + * @openapi + * /teamspaces/{teamspace}/projects/{project}/containers/{container}/revisions/{revision}/files: + * get: + * description: Downloads the container files of the selected revision + * tags: [Revisions] + * operationId: downloadContainerRevisionFiles + * parameters: + * - name: teamspace + * description: Name of teamspace + * in: path + * required: true + * schema: + * type: string + * - name: project + * description: Project ID + * in: path + * required: true + * schema: + * type: string + * format: uuid + * - name: container + * description: Container ID + * in: path + * required: true + * schema: + * type: string + * format: uuid + * - name: revision + * description: Revision ID or Revision tag + * in: path + * required: true + * schema: + * type: string + * responses: + * 401: + * $ref: "#/components/responses/notLoggedIn" + * 404: + * $ref: "#/components/responses/teamspaceNotFound" + * 200: + * description: downloads the revision files + * content: + * application/octet-stream: + * schema: + * type: string + * format: binary + */ router.get('/:revision/files', hasWriteAccessToContainer, downloadRevisionFiles(modelTypes.CONTAINER)); + + /** + * @openapi + * /teamspaces/{teamspace}/projects/{project}/containers/{container}/revisions/{revision}/files/original/info: + * get: + * description: Get the details of the original file uploaded to that revision of the container + * tags: [Revisions] + * operationId: getRevisionMD5Hash + * parameters: + * - name: teamspace + * description: Name of teamspace + * in: path + * required: true + * schema: + * type: string + * - name: project + * description: Project ID + * in: path + * required: true + * schema: + * type: string + * format: uuid + * - name: container + * description: Container ID + * in: path + * required: true + * schema: + * type: string + * format: uuid + * - name: revision + * description: Revision ID or Revision tag + * in: path + * required: true + * schema: + * type: string + * responses: + * 401: + * $ref: "#/components/responses/notLoggedIn" + * 404: + * $ref: "#/components/responses/teamspaceNotFound" + * 200: + * description: get the details of the original file uploaded to that revision of the container + * content: + * application/json: + * schema: + * type: object + * properties: + * container: + * type: string + * description: Container ID + * example: ef0855b6-4cc7-4be1-b2d6-c032dce7806a + * code: + * type: string + * description: Revision Code + * example: X01 + * uploadedAt: + * type: number + * description: Upload date + * example: 1435068682 + * hash: + * type: string + * description: MD5 hash of the original file uploaded + * example: 76dea970d89477ed03dc5289f297443c + * filename: + * type: string + * description: Name of the file + * example: test.rvt + * size: + * type: number + * description: File size in bytes + * example: 329487234 + */ + router.get('/:revision/files/original/info', hasReadAccessToContainer, getRevisionMD5Hash()); } if (modelType === modelTypes.DRAWING) { - /** - * @openapi - * /teamspaces/{teamspace}/projects/{project}/drawings/{drawing}/revisions/{revision}/files/original: - * get: - * description: Downloads the drawing files of the selected revision - * tags: [Revisions] - * operationId: downloadDrawingRevisionFiles - * parameters: - * - name: teamspace - * description: Name of teamspace - * in: path - * required: true - * schema: - * type: string - * - name: project - * description: Project ID - * in: path - * required: true - * schema: - * type: string - * format: uuid - * - name: drawing - * description: Drawing ID - * in: path - * required: true - * schema: - * type: string - * format: uuid - * - name: revision - * description: Revision ID - * in: path - * required: true - * schema: - * type: string - * format: uuid - * responses: - * 401: - * $ref: "#/components/responses/notLoggedIn" - * 404: - * $ref: "#/components/responses/teamspaceNotFound" - * 200: - * description: downloads the revision files - * content: - * application/octet-stream: - * schema: - * type: string - * format: binary - */ + /** + * @openapi + * /teamspaces/{teamspace}/projects/{project}/drawings/{drawing}/revisions/{revision}/files/original: + * get: + * description: Downloads the drawing files of the selected revision + * tags: [Revisions] + * operationId: downloadDrawingRevisionFiles + * parameters: + * - name: teamspace + * description: Name of teamspace + * in: path + * required: true + * schema: + * type: string + * - name: project + * description: Project ID + * in: path + * required: true + * schema: + * type: string + * format: uuid + * - name: drawing + * description: Drawing ID + * in: path + * required: true + * schema: + * type: string + * format: uuid + * - name: revision + * description: Revision ID + * in: path + * required: true + * schema: + * type: string + * format: uuid + * responses: + * 401: + * $ref: "#/components/responses/notLoggedIn" + * 404: + * $ref: "#/components/responses/teamspaceNotFound" + * 200: + * description: downloads the revision files + * content: + * application/octet-stream: + * schema: + * type: string + * format: binary + */ router.get('/:revision/files/original', hasWriteAccessToDrawing, downloadRevisionFiles(modelTypes.DRAWING)); /** diff --git a/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js b/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js index f14a3608781..2081977a43d 100644 --- a/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js +++ b/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js @@ -22,6 +22,7 @@ const { hasWriteAccessToFederation } = require('../../../../../middleware/permis const { respond } = require('../../../../../utils/responder'); const { templates } = require('../../../../../utils/responseCodes'); const { validateNewRevisionData } = require('../../../../../middleware/dataConverter/inputs/teamspaces/projects/models/federations'); +const { hasReadAccessToFederation } = require('../../../../../utils/permissions/permissions'); const createNewFederationRevision = async (req, res) => { const revInfo = req.body; @@ -36,6 +37,19 @@ const createNewFederationRevision = async (req, res) => { } }; +const getFederationMD5Hash = async (req, res) => { + const {teamspace, federation} = req.params; + const user = getUserFromSession(req.session) + + try { + const response = await Federations.getMD5Hash(teamspace, federation, user) + respond(req, res, templates.ok, response) + } catch (err) { + /* istanbul ignore next */ + respond(req, res, err); + } +} + const establishRoutes = () => { const router = Router({ mergeParams: true }); /** @@ -97,6 +111,95 @@ const establishRoutes = () => { * description: The request is sent successfully. */ router.post('', hasWriteAccessToFederation, validateNewRevisionData, createNewFederationRevision); + + /** + * @openapi + * /teamspaces/{teamspace}/projects/{project}/federations/{federation}/revisions/{revision}/files/original/info: + * get: + * description: Get the details of the original files uploaded to that federation + * tags: [Revisions] + * operationId: getFederationMD5Hash + * parameters: + * - name: teamspace + * description: Name of teamspace + * in: path + * required: true + * schema: + * type: string + * - name: project + * description: Project ID + * in: path + * required: true + * schema: + * type: string + * format: uuid + * - name: federation + * description: Federation ID + * in: path + * required: true + * schema: + * type: string + * format: uuid + * - name: revision + * description: Revision ID or Revision tag + * in: path + * required: true + * schema: + * type: string + * responses: + * 401: + * $ref: "#/components/responses/notLoggedIn" + * 404: + * $ref: "#/components/responses/teamspaceNotFound" + * 200: + * description: get the details of the original file uploaded to that revision of the container + * content: + * application/json: + * schema: + * type: array + * items: + * - type: object + * properties: + * container: + * type: string + * description: Container ID + * example: ef0855b6-4cc7-4be1-b2d6-c032dce7806a + * code: + * type: string + * description: Revision Code + * example: X01 + * uploadedAt: + * type: number + * description: Upload date + * example: 1435068682 + * hash: + * type: string + * description: MD5 hash of the original file uploaded + * example: 76dea970d89477ed03dc5289f297443c + * filename: + * type: string + * description: Name of the file + * example: test.rvt + * size: + * type: number + * description: File size in bytes + * example: 329487234 + * example: + * - container: ef0855b6-4cc7-4be1-b2d6-c032dce7806a + * code: X01 + * uploadedAt: 1711929600 + * hash: 14703cffd1a95017a95eb092315c3ee1 + * filename: test_1.rvt + * size: 123456789 + * - container: ef0855b6-4bb7-4be1-b2d6-c032dce7806a + * code: X01 + * uploadedAt: 1711929690 + * hash: 14703cffd1a95017a95eb092315c3ee1 + * filename: test_2.rvt + * size: 123456780 + */ + router.get('/:revision/files/original/info', hasReadAccessToFederation, getFederationMD5Hash); + return router; }; diff --git a/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js b/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js index 6c655ae6b19..7f712f83315 100644 --- a/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js +++ b/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js @@ -498,6 +498,69 @@ const testDownloadRevisionFiles = () => { }); }; +const testGetRevisionMD5Hash = () => { + describe('Get Revision MD5 Hash', () => { + const basicData = generateBasicData(); + const { users, teamspace, project, models, conRevisions } = basicData; + + beforeAll(async () => { + await setupData(basicData); + }); + + const generateTestData = () => { + let model = models.conWithRev; + let revision = conRevisions.nonVoidRevision; + let noFileRevision = conRevisions.noFileRevision; + let voidRevision = conRevisions.voidRevision; + let modelNotFound = templates.containerNotFound; + + const params = { + key: users.tsAdmin.apiKey, + ts: teamspace, + projectId: project.id, + modelId: model._id, + modelType, + revision, + }; + + return [ + ['the user does not have a valid session', { ...params, key: null }, false, templates.notLoggedIn], + ['the teamspace does not exist', { ...params, ts: ServiceHelper.generateRandomString() }, false, templates.teamspaceNotFound], + ['the user is not a member of the teamspace', { ...params, key: users.nobody.apiKey }, false, templates.teamspaceNotFound], + ['the user does not have access to the model', { ...params, key: users.noProjectAccess.apiKey }, false, templates.notAuthorized], + ['the user is viewer', { ...params, key: users.viewer.apiKey }, false, templates.notAuthorized], + ['the user is commenter', { ...params, key: users.commenter.apiKey }, false, templates.notAuthorized], + ['the project does not exist', { ...params, projectId: ServiceHelper.generateRandomString() }, false, templates.projectNotFound], + ['the model does not exist', { ...params, modelId: ServiceHelper.generateRandomString() }, false, modelNotFound], + ['the model is of wrong type', { ...params, modelId: models.federation._id }, false, modelNotFound], + ['the revision does not exist', { ...params, revision: ServiceHelper.generateRevisionEntry() }, false, templates.revisionNotFound], + ['the revision has no file', { ...params, revision: noFileRevision }, false, templates.fileNotFound], + ['the revision has a file', params, true], + ['the revision has a file (void revision)', { ...params, revision: voidRevision }, false, templates.revisionNotFound], + ] + }; + + const runTest = (desc, params, success, error) =>{ + const route = ({ts, projectId, modelId, revision, modelType, key}) => `/v5/teamspaces/${ts}/projects/${projectId}/${modelType}s/${modelId}/revisions/${revision._id}/files/original/info?key=${key}`; + + test(`should ${success? 'succeed' : `fail with ${error.code}`} if ${desc}`, async () => { + const expectedStatus = success ? templates.ok.status : error.status; + + const res = await agent.get(`${route(params)}`).expect(expectedStatus); + + if (sucess){ + expect(res.text).toEqual(params.revision.refData); + } else { + expect(res.body.code).toEqual(error.code); + } + }); + }; + + describe.each(generateTestData())(runTest) + + }) +} + const testGetImage = () => { describe('Get Image', () => { const basicData = generateBasicData(); @@ -573,4 +636,5 @@ describe(ServiceHelper.determineTestGroup(__filename), () => { testUpdateRevisionStatus(); testDownloadRevisionFiles(); testGetImage(); + testGetRevisionMD5Hash(); }); diff --git a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js index 6f513751034..ffc37d9afe3 100644 --- a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js +++ b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js @@ -180,6 +180,49 @@ const testNewRevision = () => { }); }; +const testGetFederationMD5Hash = () => { + const route = ( + teamspace, + projectId = project.id, + model = models[0]._id, + revision = ServiceHelper.generateUUIDString() + ) => `/v5/teamspaces/${teamspace}/projects/${projectId}/federations/${model}/revisions/${revision}/files/original/info` + describe('Get Federation MD5 Files', () => { + test('should fail without a valid session', async () => { + const res = await agent.get(route()).expect(templates.notLoggedIn.status); + expect(res.body.code).toEqual(templates.notLoggedIn.code); + }); + + test('should fail if the user is not a member of the teamspace', async () => { + const res = await agent.get(`${route()}?key=${nobody.apiKey}`).expect(templates.teamspaceNotFound.status); + expect(res.body.code).toEqual(templates.teamspaceNotFound.code); + }); + + test('should fail if the user does not have access to the project', async () => { + const res = await agent.get(`${route()}?key=${users.noProjectAccess.apiKey}`).expect(templates.notAuthorized.status); + expect(res.body.code).toEqual(templates.notAuthorized.code); + }); + + test('should fail if the project does not exist', async () => { + const res = await agent.get(`${route(teamspace, 'nelskfjdlsf')}?key=${users.tsAdmin.apiKey}`) + .expect(templates.projectNotFound.status); + expect(res.body.code).toEqual(templates.projectNotFound.code); + }); + + test('should fail if the federation does not exist', async () => { + const res = await agent.get(`${route(teamspace, project.id, 'sdlfkds')}?key=${users.tsAdmin.apiKey}`) + .expect(templates.federationNotFound.status); + expect(res.body.code).toEqual(templates.federationNotFound.code); + }); + + test('should succeed if correct parameters are sent', async () => { + await agent.get(`${route()}?key=${users.tsAdmin.apiKey}`) + .expect(templates.ok.status); + }); + }) +} + + describe(ServiceHelper.determineTestGroup(__filename), () => { beforeAll(async () => { server = await ServiceHelper.app(); @@ -191,4 +234,5 @@ describe(ServiceHelper.determineTestGroup(__filename), () => { ServiceHelper.closeApp(server), ])); testNewRevision(); + testGetFederationMD5Hash(); }); diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js index ee61e677208..d42d4975fdb 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js @@ -24,6 +24,8 @@ const { const fs = require('fs/promises'); const path = require('path'); +const UUIDParse = require('uuid-parse'); +const CryptoJs = require('crypto-js'); jest.mock('../../../../../../../src/v5/utils/helper/models'); const ModelHelper = require(`${src}/utils/helper/models`); @@ -43,6 +45,8 @@ const Legends = require(`${src}/models/legends`); jest.mock('../../../../../../../src/v5/models/legends'); jest.mock('../../../../../../../src/v5/services/filesManager'); const FilesManager = require(`${src}/services/filesManager`); +jest.mock('../../../../../../../src/v5/models/fileRefs') +const FileRefs = require(`${src}/models/fileRefs`) jest.mock('../../../../../../../src/v5/handler/queue'); const QueueHandler = require(`${src}/handler/queue`); @@ -431,7 +435,7 @@ const testNewRevision = () => { test('v4 compatibility test', async () => { await fs.copyFile(objModel, fileCreated); - ModelSettings.getContainerById.mockResolvedValueOnce({ }); + ModelSettings.getContainerById.mockResolvedValueOnce({}); await expect(Containers.newRevision(teamspace, model, data, file)).resolves.toBe(undefined); await expect(fileExists(fileCreated)).resolves.toBe(false); expect(QueueHandler.queueMessage).toHaveBeenCalledTimes(1); @@ -514,6 +518,56 @@ const testDownloadRevisionFiles = () => { }); }; +const testGetMD5Hash = () => { + describe('Get revision MD5 hash', () => { + test('should return empty if revision has no file', async () => { + //given: + Revisions.getRevisionByIdOrTag.mockResolvedValueOnce(templates.revisionNotFound); + + //it shoul + await expect(Containers.getRevisionMD5Hash('teamspace', 'container', generateUUIDString(), 'user1')).resolves.toEqual(); + + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1) + expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(1); + expect(FilesManager.getFile).toHaveBeenCalledTimes(0); + }) + test('should return empty if user does not have access to the revision', async () => { + // given + + //it shuld + await expect(Containers.getRevisionMD5Hash('teamspace', 'container', generateUUIDString(), 'NoAccess')).resolves.toEqual(); + + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1) + expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); + expect(FilesManager.getFile).toHaveBeenCalledTimes(0); + }) + test('should return an object if revision has file', async () => { + // given: + const revisionMock = {_id:Buffer.from('testBuffer'), rFile: ['success!'], timestamp: new Date()} + const revisionCodeMock = generateUUIDString() + const fileEntry = { size: 100, type: 'fs', link: generateRandomString() }; + + Revisions.getRevisionByIdOrTag.mockResolvedValueOnce(revisionMock); + FilesManager.getFile.mockResolvedValueOnce(revisionMock._id) + FileRefs.getRefEntry.mockResolvedValueOnce(fileEntry) + + //it should + await expect(Containers.getRevisionMD5Hash('teamspace', 'container', revisionCodeMock, 'user1')).resolves.toEqual({ + container: 'container', + code: UUIDParse.unparse(revisionMock._id.buffer), + uploadedAt: new Date(revisionMock.timestamp).getTime(), + hash: CryptoJs.MD5(revisionMock._id).toString(), + filename: revisionMock.rFile[0], + size: fileEntry.size + }); + + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1) + expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(1); + expect(FilesManager.getFile).toHaveBeenCalledTimes(1); + }) + }) +} + describe(determineTestGroup(__filename), () => { testGetContainerList(); testGetContainerStats(); @@ -526,4 +580,5 @@ describe(determineTestGroup(__filename), () => { testGetSettings(); testUpdateRevisionStatus(); testDownloadRevisionFiles(); + testGetMD5Hash(); }); diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js index ec393766297..18cf6029e7c 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js @@ -18,6 +18,9 @@ const { src } = require('../../../../../helper/path'); const { generateRandomString, generateRandomObject, determineTestGroup, generateRandomNumber } = require('../../../../../helper/services'); +const UUIDParse = require('uuid-parse'); +const CryptoJs = require('crypto-js'); + jest.mock('../../../../../../../src/v5/models/projectSettings'); const ProjectSettings = require(`${src}/models/projectSettings`); jest.mock('../../../../../../../src/v5/models/modelSettings'); @@ -36,6 +39,11 @@ const Legends = require(`${src}/models/legends`); jest.mock('../../../../../../../src/v5/models/legends'); const { templates } = require(`${src}/utils/responseCodes`); +jest.mock('../../../../../../../src/v5/services/filesManager') +const FilesManager = require(`${src}/services/filesManager`) +jest.mock('../../../../../../../src/v5/models/fileRefs') +const FilesRef = require(`${src}/models/fileRefs`) + jest.mock('../../../../../../../src/v5/utils/helper/models'); const ModelHelper = require(`${src}/utils/helper/models`); @@ -359,6 +367,100 @@ const testGetTicketGroupById = () => { }); }; +const testGetMD5Hash = () => { + describe('Get MD5 hashes for each container in the federation', () => { + const revisionMock = {_id:Buffer.from('testBuffer'), rFile: ['success!'], timestamp: new Date()} + const fileEntry = { size: 100, type: 'fs', link: generateRandomString() }; + const mockConatiners = [{_id: '1', name: 'test1', permissions: [{user: 'user1'}]},{_id: '2', name: 'test2', permissions: []},{_id: '3', name: 'test3'}] + ModelSettings.getContainers.mockImplementation((teamspace, containers, params)=>{ + if(containers.length > 1){ + return mockConatiners + } + else{ + return mockConatiners.filter(container => container._id === containers[0]) + } + } ) + + //it should get an empty array if user doesn't have rights to the container + test('should get an empty array if user does not have rights to the container', async () => { + // given + ModelSettings.getFederationById.mockResolvedValueOnce({subModels: [{_id: '1'},{_id: '2'}, {_id: '3'}]}) + + //it should + await expect(Federations.getMD5Hash('teamspace','federation', 'NoAcessUser')).resolves.toEqual([]) + + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(4) + expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); + expect(Revisions.getLatestRevision).toHaveBeenCalledTimes(0); + }) + //it should return just the containers the user has access to + test('it should return just the containers users have access to', async () => { + //given + ModelSettings.getFederationById.mockResolvedValueOnce({subModels: [{_id: '1'},{_id: '2'}, {_id: '3'}]}) + FilesManager.getFile.mockImplementation(() => revisionMock._id) + Revisions.getLatestRevision.mockResolvedValueOnce(revisionMock) + FilesRef.getRefEntry.mockResolvedValueOnce(fileEntry) + + //it should + await expect(Federations.getMD5Hash('teamspace','federation','user1')).resolves.toEqual([{container: '1', + code: UUIDParse.unparse(revisionMock._id.buffer), + uploadedAt: new Date(revisionMock.timestamp).getTime(), + hash: CryptoJs.MD5(revisionMock._id).toString(), + filename: revisionMock.rFile[0], + size: fileEntry.size}]) + + expect(ModelSettings.getFederationById).toHaveBeenCalledTimes(1) + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(4) + expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); + expect(Revisions.getLatestRevision).toHaveBeenCalledTimes(1); + expect(FilesManager.getFile).toHaveBeenCalledTimes(1) + expect(FilesRef.getRefEntry).toHaveBeenCalledTimes(1) + }) + + //it should return an array with all the containers if admin + test('it should return an array with all the containers if admin', async () => { + //given + ModelSettings.getFederationById.mockResolvedValueOnce({subModels: [{_id: '1'},{_id: '2'}, {_id: '3'}]}) + FilesManager.getFile.mockImplementation(() => revisionMock._id) + Revisions.getLatestRevision.mockResolvedValue(revisionMock) + FilesRef.getRefEntry.mockResolvedValue(fileEntry) + + //it should + await expect(Federations.getMD5Hash('teamspace','federation','tsAdmin')).resolves.toEqual([ + { + container: '1', + code: UUIDParse.unparse(revisionMock._id.buffer), + uploadedAt: new Date(revisionMock.timestamp).getTime(), + hash: CryptoJs.MD5(revisionMock._id).toString(), + filename: revisionMock.rFile[0], + size: fileEntry.size + }, { + container: '2', + code: UUIDParse.unparse(revisionMock._id.buffer), + uploadedAt: new Date(revisionMock.timestamp).getTime(), + hash: CryptoJs.MD5(revisionMock._id).toString(), + filename: revisionMock.rFile[0], + size: fileEntry.size + }, { + container: '3', + code: UUIDParse.unparse(revisionMock._id.buffer), + uploadedAt: new Date(revisionMock.timestamp).getTime(), + hash: CryptoJs.MD5(revisionMock._id).toString(), + filename: revisionMock.rFile[0], + size: fileEntry.size + }]) + + expect(ModelSettings.getFederationById).toHaveBeenCalledTimes(1) + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(4) + expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); + expect(Revisions.getLatestRevision).toHaveBeenCalledTimes(3); + expect(FilesManager.getFile).toHaveBeenCalledTimes(3) + expect(FilesRef.getRefEntry).toHaveBeenCalledTimes(3) + }) + + }) +} + describe(determineTestGroup(__filename), () => { testGetFederationList(); testAppendFavourites(); @@ -368,4 +470,5 @@ describe(determineTestGroup(__filename), () => { testDeleteFederation(); testGetSettings(); testGetTicketGroupById(); + testGetMD5Hash(); }); From f3c8bd4e162430929c7973b7040e9b51a9fe0a38 Mon Sep 17 00:00:00 2001 From: Bogdan Cornea Date: Mon, 23 Dec 2024 12:20:39 +0000 Subject: [PATCH 02/15] eslint fix --- .../projects/models/commons/modelList.js | 43 +++---- .../teamspaces/projects/models/containers.js | 7 +- .../teamspaces/projects/models/federations.js | 23 ++-- .../projects/models/common/revisions.js | 12 +- .../projects/models/federations/revisions.js | 10 +- .../projects/models/common/revisions.test.js | 31 +++-- .../models/federations/revisions.test.js | 9 +- .../projects/models/containers.test.js | 40 +++---- .../projects/models/federations.test.js | 110 +++++++++--------- 9 files changed, 142 insertions(+), 143 deletions(-) diff --git a/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js b/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js index 749392c97d1..c2cee39f491 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js +++ b/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js @@ -15,19 +15,18 @@ * along with this program. If not, see . */ +const { addModel, getContainers } = require('../../../../../models/modelSettings'); const { addModelToProject, getProjectById, removeModelFromProject } = require('../../../../../models/projectSettings'); +const { getLatestRevision, getRevisionByIdOrTag } = require('../../../../../models/revisions'); const { hasProjectAdminPermissions, isTeamspaceAdmin } = require('../../../../../utils/permissions/permissions'); +const CryptoJs = require('crypto-js'); const { USERS_DB_NAME } = require('../../../../../models/users.constants'); -const { addModel, getContainers } = require('../../../../../models/modelSettings'); -const { modelTypes } = require('../../../../../models/modelSettings.constants'); +const UUIDParse = require('uuid-parse'); const { getFavourites } = require('../../../../../models/users'); +const { getFile } = require('../../../../../services/filesManager'); +const { getRefEntry } = require('../../../../../models/fileRefs'); +const { modelTypes } = require('../../../../../models/modelSettings.constants'); const { removeModelData } = require('../../../../../utils/helper/models'); -const UUIDParse = require('uuid-parse'); -const CryptoJs = require('crypto-js'); -const { getFile } = require('../../../../../services/filesManager') -const { getLatestRevision, getRevisionByIdOrTag } = require('../../../../../models/revisions') -const { getRefEntry } = require('../../../../../models/fileRefs') - const ModelList = {}; @@ -61,7 +60,7 @@ ModelList.getModelList = async (teamspace, project, user, modelSettings) => { _id, name, role: isAdmin ? USERS_DB_NAME : perm.permission, - isFavourite: favourites.includes(_id) + isFavourite: favourites.includes(_id), }; }); }; @@ -69,21 +68,26 @@ ModelList.getModelList = async (teamspace, project, user, modelSettings) => { ModelList.getModelMD5Hash = async (teamspace, container, revision, user) => { const [isAdmin, containers] = await Promise.all([ isTeamspaceAdmin(teamspace, user), - getContainers(teamspace, [container], { _id: 1, name: 1, permissions: 1 }) - ]) + getContainers(teamspace, [container], { _id: 1, name: 1, permissions: 1 }), + ]); let rev; - //if not allowed just return nothing - if (!isAdmin && !containers[0].permissions?.some(permission => permission?.user === user)) return; + // if not allowed just return nothing + if (!isAdmin && !containers[0].permissions?.some((permission) => permission?.user === user)) return; - //retrieve the right revision + // retrieve the right revision if (revision?.length) { - rev = await getRevisionByIdOrTag(teamspace, container, modelTypes.CONTAINER, revision, { rFile: 1, timestamp: 1, fileSize: 1 }, { includeVoid: false }) + rev = await getRevisionByIdOrTag( + teamspace, container, modelTypes.CONTAINER, revision, + { rFile: 1, timestamp: 1, fileSize: 1 }, + { includeVoid: false }); } else { - rev = await getLatestRevision(teamspace, container, modelTypes.CONTAINER, { rFile: 1, timestamp: 1, fileSize: 1 }) + rev = await getLatestRevision( + teamspace, container, modelTypes.CONTAINER, + { rFile: 1, timestamp: 1, fileSize: 1 }); } - //check if anything is in there + // check if anything is in there if (!rev.rFile?.length) return; const filename = rev.rFile[0]; @@ -101,9 +105,8 @@ ModelList.getModelMD5Hash = async (teamspace, container, revision, user) => { uploadedAt, hash, filename, - size: refEntry.size + size: refEntry.size, }; - -} +}; module.exports = ModelList; diff --git a/backend/src/v5/processors/teamspaces/projects/models/containers.js b/backend/src/v5/processors/teamspaces/projects/models/containers.js index bc7d1e6dced..90aa83182f5 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/containers.js +++ b/backend/src/v5/processors/teamspaces/projects/models/containers.js @@ -34,7 +34,6 @@ const { queueModelUpload } = require('../../../../services/modelProcessing'); const { templates } = require('../../../../utils/responseCodes'); const { timestampToString } = require('../../../../utils/helper/dates'); - const Containers = { ...Groups, ...Views, ...Tickets, ...Comments, ...TicketGroups }; Containers.addContainer = addModel; @@ -134,9 +133,9 @@ Containers.updateSettings = updateModelSettings; Containers.getSettings = (teamspace, container) => getContainerById(teamspace, container, { corID: 0, account: 0, permissions: 0 }); -Containers.getRevisionMD5Hash = async (teamspace, container, revision, user) =>{ - const response = await getModelMD5Hash(teamspace, container, revision, user) +Containers.getRevisionMD5Hash = async (teamspace, container, revision, user) => { + const response = await getModelMD5Hash(teamspace, container, revision, user); return response; -} +}; module.exports = Containers; diff --git a/backend/src/v5/processors/teamspaces/projects/models/federations.js b/backend/src/v5/processors/teamspaces/projects/models/federations.js index fdae212da0a..576a0850744 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/federations.js +++ b/backend/src/v5/processors/teamspaces/projects/models/federations.js @@ -24,11 +24,11 @@ const TicketGroups = require('./commons/tickets.groups'); const Tickets = require('./commons/tickets'); const Views = require('./commons/views'); const { getLatestRevision } = require('../../../../models/revisions'); +const { getModelMD5Hash } = require('./commons/modelList'); const { getOpenTicketsCount } = require('./commons/tickets'); const { getProjectById } = require('../../../../models/projectSettings'); const { modelTypes } = require('../../../../models/modelSettings.constants'); const { queueFederationUpdate } = require('../../../../services/modelProcessing'); -const { getModelMD5Hash } = require('../../projects/models/commons/modelList') const Federations = { ...Groups, ...Views, ...Tickets, ...Comments, ...TicketGroups }; @@ -110,19 +110,20 @@ Federations.getSettings = (teamspace, federation) => getFederationById(teamspace Federations.getMD5Hash = async (teamspace, federation, user) => { const { subModels: containers } = await getFederationById(teamspace, federation, { subModels: 1 }); - const containerWithMetadata = await getContainers(teamspace, containers.map(container => container._id), { _id: 1, name: 1, permissions: 1 }) + const containerWithMetadata = await getContainers( + teamspace, + containers.map((container) => container._id), + { _id: 1, name: 1, permissions: 1 }); - const listOfPromises = containerWithMetadata.map(container => { - return getModelMD5Hash(teamspace, container._id, null, user) - }) + const listOfPromises = containerWithMetadata.map( + (container) => getModelMD5Hash(teamspace, container._id, null, user)); - const promiseResponses = await Promise.allSettled(listOfPromises) + const promiseResponses = await Promise.allSettled(listOfPromises); const responses = promiseResponses - .filter(response => response.status === 'fulfilled' && response.value !== undefined) - .map(response => response.value) + .filter((response) => response.status === 'fulfilled' && response.value !== undefined) + .map((response) => response.value); - return responses - -} + return responses; +}; module.exports = Federations; diff --git a/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js b/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js index 41e56e252d6..15e62dd4c61 100644 --- a/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js +++ b/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js @@ -60,17 +60,17 @@ const getRevisions = (modelType) => async (req, res, next) => { }; const getRevisionMD5Hash = () => async (req, res) => { - const { teamspace, container, revision } = req.params - const user = getUserFromSession(req.session) + const { teamspace, container, revision } = req.params; + const user = getUserFromSession(req.session); try { - const response = await Containers.getRevisionMD5Hash(teamspace, container, revision, user) - respond(req, res, templates.ok, response) + const response = await Containers.getRevisionMD5Hash(teamspace, container, revision, user); + respond(req, res, templates.ok, response); } catch (err) { /* istanbul ignore next */ - respond(req, res, err) + respond(req, res, err); } -} +}; const updateRevisionStatus = (modelType) => async (req, res) => { const { teamspace, project, model, revision } = req.params; diff --git a/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js b/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js index 2081977a43d..9802a7560b3 100644 --- a/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js +++ b/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js @@ -38,17 +38,17 @@ const createNewFederationRevision = async (req, res) => { }; const getFederationMD5Hash = async (req, res) => { - const {teamspace, federation} = req.params; - const user = getUserFromSession(req.session) + const { teamspace, federation } = req.params; + const user = getUserFromSession(req.session); try { - const response = await Federations.getMD5Hash(teamspace, federation, user) - respond(req, res, templates.ok, response) + const response = await Federations.getMD5Hash(teamspace, federation, user); + respond(req, res, templates.ok, response); } catch (err) { /* istanbul ignore next */ respond(req, res, err); } -} +}; const establishRoutes = () => { const router = Router({ mergeParams: true }); diff --git a/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js b/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js index 7f712f83315..897431cad18 100644 --- a/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js +++ b/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js @@ -508,11 +508,11 @@ const testGetRevisionMD5Hash = () => { }); const generateTestData = () => { - let model = models.conWithRev; - let revision = conRevisions.nonVoidRevision; - let noFileRevision = conRevisions.noFileRevision; - let voidRevision = conRevisions.voidRevision; - let modelNotFound = templates.containerNotFound; + const model = models.conWithRev; + const revision = conRevisions.nonVoidRevision; + const { noFileRevision } = conRevisions; + const { voidRevision } = conRevisions; + const modelNotFound = templates.containerNotFound; const params = { key: users.tsAdmin.apiKey, @@ -537,29 +537,28 @@ const testGetRevisionMD5Hash = () => { ['the revision has no file', { ...params, revision: noFileRevision }, false, templates.fileNotFound], ['the revision has a file', params, true], ['the revision has a file (void revision)', { ...params, revision: voidRevision }, false, templates.revisionNotFound], - ] + ]; }; - const runTest = (desc, params, success, error) =>{ - const route = ({ts, projectId, modelId, revision, modelType, key}) => `/v5/teamspaces/${ts}/projects/${projectId}/${modelType}s/${modelId}/revisions/${revision._id}/files/original/info?key=${key}`; + const runTest = (desc, params, success, error) => { + const route = ({ ts, projectId, modelId, revision, modelType, key }) => `/v5/teamspaces/${ts}/projects/${projectId}/${modelType}s/${modelId}/revisions/${revision._id}/files/original/info?key=${key}`; - test(`should ${success? 'succeed' : `fail with ${error.code}`} if ${desc}`, async () => { + test(`should ${success ? 'succeed' : `fail with ${error.code}`} if ${desc}`, async () => { const expectedStatus = success ? templates.ok.status : error.status; const res = await agent.get(`${route(params)}`).expect(expectedStatus); - if (sucess){ - expect(res.text).toEqual(params.revision.refData); + if (sucess) { + expect(res.text).toEqual(params.revision.refData); } else { - expect(res.body.code).toEqual(error.code); + expect(res.body.code).toEqual(error.code); } }); }; - describe.each(generateTestData())(runTest) - - }) -} + describe.each(generateTestData())(runTest); + }); +}; const testGetImage = () => { describe('Get Image', () => { diff --git a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js index ffc37d9afe3..d973d44b536 100644 --- a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js +++ b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js @@ -185,8 +185,8 @@ const testGetFederationMD5Hash = () => { teamspace, projectId = project.id, model = models[0]._id, - revision = ServiceHelper.generateUUIDString() - ) => `/v5/teamspaces/${teamspace}/projects/${projectId}/federations/${model}/revisions/${revision}/files/original/info` + revision = ServiceHelper.generateUUIDString(), + ) => `/v5/teamspaces/${teamspace}/projects/${projectId}/federations/${model}/revisions/${revision}/files/original/info`; describe('Get Federation MD5 Files', () => { test('should fail without a valid session', async () => { const res = await agent.get(route()).expect(templates.notLoggedIn.status); @@ -219,9 +219,8 @@ const testGetFederationMD5Hash = () => { await agent.get(`${route()}?key=${users.tsAdmin.apiKey}`) .expect(templates.ok.status); }); - }) -} - + }); +}; describe(ServiceHelper.determineTestGroup(__filename), () => { beforeAll(async () => { diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js index d42d4975fdb..11aa4573a45 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js @@ -45,8 +45,8 @@ const Legends = require(`${src}/models/legends`); jest.mock('../../../../../../../src/v5/models/legends'); jest.mock('../../../../../../../src/v5/services/filesManager'); const FilesManager = require(`${src}/services/filesManager`); -jest.mock('../../../../../../../src/v5/models/fileRefs') -const FileRefs = require(`${src}/models/fileRefs`) +jest.mock('../../../../../../../src/v5/models/fileRefs'); +const FileRefs = require(`${src}/models/fileRefs`); jest.mock('../../../../../../../src/v5/handler/queue'); const QueueHandler = require(`${src}/handler/queue`); @@ -521,52 +521,52 @@ const testDownloadRevisionFiles = () => { const testGetMD5Hash = () => { describe('Get revision MD5 hash', () => { test('should return empty if revision has no file', async () => { - //given: + // given: Revisions.getRevisionByIdOrTag.mockResolvedValueOnce(templates.revisionNotFound); - //it shoul + // it shoul await expect(Containers.getRevisionMD5Hash('teamspace', 'container', generateUUIDString(), 'user1')).resolves.toEqual(); - expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1) + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(1); expect(FilesManager.getFile).toHaveBeenCalledTimes(0); - }) + }); test('should return empty if user does not have access to the revision', async () => { // given - //it shuld + // it shuld await expect(Containers.getRevisionMD5Hash('teamspace', 'container', generateUUIDString(), 'NoAccess')).resolves.toEqual(); - expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1) + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); expect(FilesManager.getFile).toHaveBeenCalledTimes(0); - }) + }); test('should return an object if revision has file', async () => { // given: - const revisionMock = {_id:Buffer.from('testBuffer'), rFile: ['success!'], timestamp: new Date()} - const revisionCodeMock = generateUUIDString() + const revisionMock = { _id: Buffer.from('testBuffer'), rFile: ['success!'], timestamp: new Date() }; + const revisionCodeMock = generateUUIDString(); const fileEntry = { size: 100, type: 'fs', link: generateRandomString() }; - + Revisions.getRevisionByIdOrTag.mockResolvedValueOnce(revisionMock); - FilesManager.getFile.mockResolvedValueOnce(revisionMock._id) - FileRefs.getRefEntry.mockResolvedValueOnce(fileEntry) + FilesManager.getFile.mockResolvedValueOnce(revisionMock._id); + FileRefs.getRefEntry.mockResolvedValueOnce(fileEntry); - //it should + // it should await expect(Containers.getRevisionMD5Hash('teamspace', 'container', revisionCodeMock, 'user1')).resolves.toEqual({ container: 'container', code: UUIDParse.unparse(revisionMock._id.buffer), uploadedAt: new Date(revisionMock.timestamp).getTime(), hash: CryptoJs.MD5(revisionMock._id).toString(), filename: revisionMock.rFile[0], - size: fileEntry.size + size: fileEntry.size, }); - expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1) + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(1); expect(FilesManager.getFile).toHaveBeenCalledTimes(1); - }) - }) -} + }); + }); +}; describe(determineTestGroup(__filename), () => { testGetContainerList(); diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js index 18cf6029e7c..bf09b236076 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js @@ -39,10 +39,10 @@ const Legends = require(`${src}/models/legends`); jest.mock('../../../../../../../src/v5/models/legends'); const { templates } = require(`${src}/utils/responseCodes`); -jest.mock('../../../../../../../src/v5/services/filesManager') -const FilesManager = require(`${src}/services/filesManager`) -jest.mock('../../../../../../../src/v5/models/fileRefs') -const FilesRef = require(`${src}/models/fileRefs`) +jest.mock('../../../../../../../src/v5/services/filesManager'); +const FilesManager = require(`${src}/services/filesManager`); +jest.mock('../../../../../../../src/v5/models/fileRefs'); +const FilesRef = require(`${src}/models/fileRefs`); jest.mock('../../../../../../../src/v5/utils/helper/models'); const ModelHelper = require(`${src}/utils/helper/models`); @@ -369,97 +369,95 @@ const testGetTicketGroupById = () => { const testGetMD5Hash = () => { describe('Get MD5 hashes for each container in the federation', () => { - const revisionMock = {_id:Buffer.from('testBuffer'), rFile: ['success!'], timestamp: new Date()} + const revisionMock = { _id: Buffer.from('testBuffer'), rFile: ['success!'], timestamp: new Date() }; const fileEntry = { size: 100, type: 'fs', link: generateRandomString() }; - const mockConatiners = [{_id: '1', name: 'test1', permissions: [{user: 'user1'}]},{_id: '2', name: 'test2', permissions: []},{_id: '3', name: 'test3'}] - ModelSettings.getContainers.mockImplementation((teamspace, containers, params)=>{ - if(containers.length > 1){ - return mockConatiners + const mockConatiners = [{ _id: '1', name: 'test1', permissions: [{ user: 'user1' }] }, { _id: '2', name: 'test2', permissions: [] }, { _id: '3', name: 'test3' }]; + ModelSettings.getContainers.mockImplementation((teamspace, containers, params) => { + if (containers.length > 1) { + return mockConatiners; } - else{ - return mockConatiners.filter(container => container._id === containers[0]) - } - } ) - //it should get an empty array if user doesn't have rights to the container + return mockConatiners.filter((container) => container._id === containers[0]); + }); + + // it should get an empty array if user doesn't have rights to the container test('should get an empty array if user does not have rights to the container', async () => { // given - ModelSettings.getFederationById.mockResolvedValueOnce({subModels: [{_id: '1'},{_id: '2'}, {_id: '3'}]}) - - //it should - await expect(Federations.getMD5Hash('teamspace','federation', 'NoAcessUser')).resolves.toEqual([]) + ModelSettings.getFederationById.mockResolvedValueOnce({ subModels: [{ _id: '1' }, { _id: '2' }, { _id: '3' }] }); + + // it should + await expect(Federations.getMD5Hash('teamspace', 'federation', 'NoAcessUser')).resolves.toEqual([]); - expect(ModelSettings.getContainers).toHaveBeenCalledTimes(4) + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(4); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); expect(Revisions.getLatestRevision).toHaveBeenCalledTimes(0); - }) - //it should return just the containers the user has access to + }); + // it should return just the containers the user has access to test('it should return just the containers users have access to', async () => { - //given - ModelSettings.getFederationById.mockResolvedValueOnce({subModels: [{_id: '1'},{_id: '2'}, {_id: '3'}]}) - FilesManager.getFile.mockImplementation(() => revisionMock._id) - Revisions.getLatestRevision.mockResolvedValueOnce(revisionMock) - FilesRef.getRefEntry.mockResolvedValueOnce(fileEntry) - - //it should - await expect(Federations.getMD5Hash('teamspace','federation','user1')).resolves.toEqual([{container: '1', + // given + ModelSettings.getFederationById.mockResolvedValueOnce({ subModels: [{ _id: '1' }, { _id: '2' }, { _id: '3' }] }); + FilesManager.getFile.mockImplementation(() => revisionMock._id); + Revisions.getLatestRevision.mockResolvedValueOnce(revisionMock); + FilesRef.getRefEntry.mockResolvedValueOnce(fileEntry); + + // it should + await expect(Federations.getMD5Hash('teamspace', 'federation', 'user1')).resolves.toEqual([{ container: '1', code: UUIDParse.unparse(revisionMock._id.buffer), uploadedAt: new Date(revisionMock.timestamp).getTime(), hash: CryptoJs.MD5(revisionMock._id).toString(), filename: revisionMock.rFile[0], - size: fileEntry.size}]) + size: fileEntry.size }]); - expect(ModelSettings.getFederationById).toHaveBeenCalledTimes(1) - expect(ModelSettings.getContainers).toHaveBeenCalledTimes(4) + expect(ModelSettings.getFederationById).toHaveBeenCalledTimes(1); + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(4); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); expect(Revisions.getLatestRevision).toHaveBeenCalledTimes(1); - expect(FilesManager.getFile).toHaveBeenCalledTimes(1) - expect(FilesRef.getRefEntry).toHaveBeenCalledTimes(1) - }) + expect(FilesManager.getFile).toHaveBeenCalledTimes(1); + expect(FilesRef.getRefEntry).toHaveBeenCalledTimes(1); + }); - //it should return an array with all the containers if admin + // it should return an array with all the containers if admin test('it should return an array with all the containers if admin', async () => { - //given - ModelSettings.getFederationById.mockResolvedValueOnce({subModels: [{_id: '1'},{_id: '2'}, {_id: '3'}]}) - FilesManager.getFile.mockImplementation(() => revisionMock._id) - Revisions.getLatestRevision.mockResolvedValue(revisionMock) - FilesRef.getRefEntry.mockResolvedValue(fileEntry) - - //it should - await expect(Federations.getMD5Hash('teamspace','federation','tsAdmin')).resolves.toEqual([ + // given + ModelSettings.getFederationById.mockResolvedValueOnce({ subModels: [{ _id: '1' }, { _id: '2' }, { _id: '3' }] }); + FilesManager.getFile.mockImplementation(() => revisionMock._id); + Revisions.getLatestRevision.mockResolvedValue(revisionMock); + FilesRef.getRefEntry.mockResolvedValue(fileEntry); + + // it should + await expect(Federations.getMD5Hash('teamspace', 'federation', 'tsAdmin')).resolves.toEqual([ { container: '1', code: UUIDParse.unparse(revisionMock._id.buffer), uploadedAt: new Date(revisionMock.timestamp).getTime(), hash: CryptoJs.MD5(revisionMock._id).toString(), filename: revisionMock.rFile[0], - size: fileEntry.size + size: fileEntry.size, }, { container: '2', code: UUIDParse.unparse(revisionMock._id.buffer), uploadedAt: new Date(revisionMock.timestamp).getTime(), hash: CryptoJs.MD5(revisionMock._id).toString(), filename: revisionMock.rFile[0], - size: fileEntry.size + size: fileEntry.size, }, { container: '3', code: UUIDParse.unparse(revisionMock._id.buffer), uploadedAt: new Date(revisionMock.timestamp).getTime(), hash: CryptoJs.MD5(revisionMock._id).toString(), filename: revisionMock.rFile[0], - size: fileEntry.size - }]) + size: fileEntry.size, + }]); - expect(ModelSettings.getFederationById).toHaveBeenCalledTimes(1) - expect(ModelSettings.getContainers).toHaveBeenCalledTimes(4) + expect(ModelSettings.getFederationById).toHaveBeenCalledTimes(1); + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(4); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); expect(Revisions.getLatestRevision).toHaveBeenCalledTimes(3); - expect(FilesManager.getFile).toHaveBeenCalledTimes(3) - expect(FilesRef.getRefEntry).toHaveBeenCalledTimes(3) - }) - - }) -} + expect(FilesManager.getFile).toHaveBeenCalledTimes(3); + expect(FilesRef.getRefEntry).toHaveBeenCalledTimes(3); + }); + }); +}; describe(determineTestGroup(__filename), () => { testGetFederationList(); From 0f0b8984a09f291a26436e6803460b5ea9960c6c Mon Sep 17 00:00:00 2001 From: Bogdan Cornea Date: Mon, 30 Dec 2024 16:24:31 +0000 Subject: [PATCH 03/15] update tests and lint adjustments --- .../projects/models/commons/modelList.js | 9 +- .../teamspaces/projects/models/federations.js | 1 + .../projects/models/federations/revisions.js | 16 +-- .../projects/models/common/revisions.test.js | 51 ++++---- .../models/federations/revisions.test.js | 119 +++++++++++++----- .../projects/models/federations.test.js | 2 +- 6 files changed, 130 insertions(+), 68 deletions(-) diff --git a/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js b/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js index c2cee39f491..d569db1f056 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js +++ b/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js @@ -71,9 +71,10 @@ ModelList.getModelMD5Hash = async (teamspace, container, revision, user) => { getContainers(teamspace, [container], { _id: 1, name: 1, permissions: 1 }), ]); let rev; + let returnValue; // if not allowed just return nothing - if (!isAdmin && !containers[0].permissions?.some((permission) => permission?.user === user)) return; + if (!isAdmin && !containers[0].permissions?.some((permission) => permission?.user === user)) return returnValue; // retrieve the right revision if (revision?.length) { @@ -88,7 +89,7 @@ ModelList.getModelMD5Hash = async (teamspace, container, revision, user) => { } // check if anything is in there - if (!rev.rFile?.length) return; + if (!rev.rFile?.length) return returnValue; const filename = rev.rFile[0]; @@ -99,7 +100,7 @@ ModelList.getModelMD5Hash = async (teamspace, container, revision, user) => { const code = UUIDParse.unparse(rev._id.buffer); const uploadedAt = new Date(rev.timestamp).getTime(); - return { + returnValue = { container, code, uploadedAt, @@ -107,6 +108,8 @@ ModelList.getModelMD5Hash = async (teamspace, container, revision, user) => { filename, size: refEntry.size, }; + + return returnValue; }; module.exports = ModelList; diff --git a/backend/src/v5/processors/teamspaces/projects/models/federations.js b/backend/src/v5/processors/teamspaces/projects/models/federations.js index 576a0850744..3dca9a1ed03 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/federations.js +++ b/backend/src/v5/processors/teamspaces/projects/models/federations.js @@ -120,6 +120,7 @@ Federations.getMD5Hash = async (teamspace, federation, user) => { const promiseResponses = await Promise.allSettled(listOfPromises); const responses = promiseResponses + // make sure the promise is fulfilled and the value is not undefined or empty .filter((response) => response.status === 'fulfilled' && response.value !== undefined) .map((response) => response.value); diff --git a/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js b/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js index 9802a7560b3..cef21c404ec 100644 --- a/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js +++ b/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js @@ -18,11 +18,11 @@ const Federations = require('../../../../../processors/teamspaces/projects/models/federations'); const { Router } = require('express'); const { getUserFromSession } = require('../../../../../utils/sessions'); +const { hasReadAccessToFederation } = require('../../../../../utils/permissions/permissions'); const { hasWriteAccessToFederation } = require('../../../../../middleware/permissions/permissions'); const { respond } = require('../../../../../utils/responder'); const { templates } = require('../../../../../utils/responseCodes'); const { validateNewRevisionData } = require('../../../../../middleware/dataConverter/inputs/teamspaces/projects/models/federations'); -const { hasReadAccessToFederation } = require('../../../../../utils/permissions/permissions'); const createNewFederationRevision = async (req, res) => { const revInfo = req.body; @@ -154,7 +154,7 @@ const establishRoutes = () => { * 200: * description: get the details of the original file uploaded to that revision of the container * content: - * application/json: + * application/json: * schema: * type: array * items: @@ -167,24 +167,24 @@ const establishRoutes = () => { * code: * type: string * description: Revision Code - * example: X01 + * example: X01 * uploadedAt: * type: number * description: Upload date - * example: 1435068682 + * example: 1435068682 * hash: * type: string * description: MD5 hash of the original file uploaded - * example: 76dea970d89477ed03dc5289f297443c + * example: 76dea970d89477ed03dc5289f297443c * filename: * type: string * description: Name of the file - * example: test.rvt + * example: test.rvt * size: * type: number * description: File size in bytes - * example: 329487234 - * example: + * example: 329487234 + * example: * - container: ef0855b6-4cc7-4be1-b2d6-c032dce7806a * code: X01 * uploadedAt: 1711929600 diff --git a/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js b/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js index 897431cad18..2a1fa5b94ba 100644 --- a/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js +++ b/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js @@ -19,6 +19,7 @@ const SuperTest = require('supertest'); const ServiceHelper = require('../../../../../../helper/services'); const { src, dwgModel, dwgModelUppercaseExt, image } = require('../../../../../../helper/path'); const { writeFileSync, unlinkSync } = require('fs'); +const CryptoJs = require('crypto-js'); const { deleteIfUndefined } = require(`${src}/utils/helper/objects`); const { modelTypes, statusCodes } = require(`${src}/models/modelSettings.constants`); @@ -510,53 +511,57 @@ const testGetRevisionMD5Hash = () => { const generateTestData = () => { const model = models.conWithRev; const revision = conRevisions.nonVoidRevision; - const { noFileRevision } = conRevisions; const { voidRevision } = conRevisions; - const modelNotFound = templates.containerNotFound; + + const MD5HashResponseExpectation = { + container: model._id, + code: revision._id, + uploadedAt: new Date(revision.timestamp).getTime(), + hash: CryptoJs.MD5(Buffer.from(revision.rFile[0])).toString(), + filename: revision.rFile[0], + size: 20, + }; const params = { key: users.tsAdmin.apiKey, ts: teamspace, projectId: project.id, modelId: model._id, - modelType, - revision, + revision: { ...conRevisions.nonVoidRevision, refData: MD5HashResponseExpectation }, }; return [ - ['the user does not have a valid session', { ...params, key: null }, false, templates.notLoggedIn], - ['the teamspace does not exist', { ...params, ts: ServiceHelper.generateRandomString() }, false, templates.teamspaceNotFound], - ['the user is not a member of the teamspace', { ...params, key: users.nobody.apiKey }, false, templates.teamspaceNotFound], - ['the user does not have access to the model', { ...params, key: users.noProjectAccess.apiKey }, false, templates.notAuthorized], - ['the user is viewer', { ...params, key: users.viewer.apiKey }, false, templates.notAuthorized], - ['the user is commenter', { ...params, key: users.commenter.apiKey }, false, templates.notAuthorized], - ['the project does not exist', { ...params, projectId: ServiceHelper.generateRandomString() }, false, templates.projectNotFound], - ['the model does not exist', { ...params, modelId: ServiceHelper.generateRandomString() }, false, modelNotFound], - ['the model is of wrong type', { ...params, modelId: models.federation._id }, false, modelNotFound], - ['the revision does not exist', { ...params, revision: ServiceHelper.generateRevisionEntry() }, false, templates.revisionNotFound], - ['the revision has no file', { ...params, revision: noFileRevision }, false, templates.fileNotFound], - ['the revision has a file', params, true], - ['the revision has a file (void revision)', { ...params, revision: voidRevision }, false, templates.revisionNotFound], + ['the user does not have a valid session.', { ...params, key: null }, false, templates.notLoggedIn], + ['the teamspace does not exist.', { ...params, ts: 'not a valid ts' }, false, templates.teamspaceNotFound], + ['the user is not a member of the teamspace.', { ...params, key: users.nobody.apiKey }, false, templates.teamspaceNotFound], + ['the user does not have access to the model.', { ...params, key: users.noProjectAccess.apiKey }, false, templates.notAuthorized], + ['the user is viewer.', { ...params, key: users.viewer.apiKey }, true], + ['the user is commenter.', { ...params, key: users.commenter.apiKey }, true], + ['the project does not exist.', { ...params, projectId: ServiceHelper.generateRandomString() }, false, templates.projectNotFound], + ['the model does not exist.', { ...params, modelId: ServiceHelper.generateRandomString() }, false, templates.containerNotFound], + ['the model is of wrong type.', { ...params, modelId: models.federation._id }, false, templates.containerNotFound], + ['the revision does not exist.', { ...params, revision: ServiceHelper.generateRevisionEntry() }, false, templates.revisionNotFound], + ['the revision has a file.', params, true], + ['the revision has a file (void revision).', { ...params, revision: voidRevision }, false, templates.revisionNotFound], ]; }; const runTest = (desc, params, success, error) => { - const route = ({ ts, projectId, modelId, revision, modelType, key }) => `/v5/teamspaces/${ts}/projects/${projectId}/${modelType}s/${modelId}/revisions/${revision._id}/files/original/info?key=${key}`; + const route = ({ ts, projectId, modelId, revision, key }) => `/v5/teamspaces/${ts}/projects/${projectId}/containers/${modelId}/revisions/${revision._id}/files/original/info?key=${key}`; test(`should ${success ? 'succeed' : `fail with ${error.code}`} if ${desc}`, async () => { const expectedStatus = success ? templates.ok.status : error.status; - const res = await agent.get(`${route(params)}`).expect(expectedStatus); - if (sucess) { - expect(res.text).toEqual(params.revision.refData); + if (success) { + expect(res.text).toEqual(JSON.stringify(params.revision.refData)); } else { expect(res.body.code).toEqual(error.code); } }); }; - describe.each(generateTestData())(runTest); + describe.each(generateTestData())('Container', runTest); }); }; @@ -585,7 +590,7 @@ const testGetImage = () => { return [ ['the user does not have a valid session', { ...params, key: null }, false, templates.notLoggedIn], - ['the teamspace does not exist', { ...params, ts: ServiceHelper.generateRandomString() }, false, templates.teamspaceNotFound], + ['the teamspace does not exist', { ...params, ts: 'notAvalidTS' }, false, templates.teamspaceNotFound], ['the user is not a member of the teamspace', { ...params, key: users.nobody.apiKey }, false, templates.teamspaceNotFound], ['the user does not have access to the model', { ...params, key: users.noProjectAccess.apiKey }, false, templates.notAuthorized], ['the project does not exist', { ...params, projectId: ServiceHelper.generateRandomString() }, false, templates.projectNotFound], diff --git a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js index d973d44b536..bc5e9142ce5 100644 --- a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js +++ b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js @@ -18,8 +18,10 @@ const SuperTest = require('supertest'); const ServiceHelper = require('../../../../../../helper/services'); const { src } = require('../../../../../../helper/path'); +const CryptoJs = require('crypto-js'); const { templates } = require(`${src}/utils/responseCodes`); +const { modelTypes } = require(`${src}/models/modelSettings.constants`); let server; let agent; @@ -39,6 +41,20 @@ const project = { id: ServiceHelper.generateUUIDString(), name: ServiceHelper.generateRandomString(), }; +const containers = [{ + _id: ServiceHelper.generateUUIDString(), + name: ServiceHelper.generateRandomString(), + properties: { + ...ServiceHelper.generateRandomModelProperties(modelTypes.CONTAINER), + permissions: [{ user: users.viewer.user, permission: 'viewer' }, { user: users.commenter.user, permission: 'commenter' }], + }, +}, { + _id: ServiceHelper.generateUUIDString(), + name: ServiceHelper.generateRandomString(), + properties: { + ...ServiceHelper.generateRandomModelProperties(modelTypes.CONTAINER), + }, +}]; const models = [ { @@ -48,15 +64,17 @@ const models = [ ...ServiceHelper.generateRandomModelProperties(), permissions: [{ user: users.viewer, permission: 'viewer' }, { user: users.commenter, permission: 'commenter' }], federate: true, + subModels: containers.map((model) => ({ _id: model._id })), }, }, { _id: ServiceHelper.generateUUIDString(), name: ServiceHelper.generateRandomString(), properties: { - ...ServiceHelper.generateRandomModelProperties(), + ...ServiceHelper.generateRandomModelProperties(modelTypes.FEDERATION), permissions: [{ user: users.viewer, permission: 'viewer' }, { user: users.commenter, permission: 'commenter' }], federate: true, + subModels: containers.map((model) => ({ _id: model._id })), }, }, { @@ -68,6 +86,7 @@ const models = [ const container = models[2]; const anotherFed = models[1]; +const conRevisions = ServiceHelper.generateRevisionEntry(); const setupData = async () => { await ServiceHelper.db.createTeamspace(teamspace, [users.tsAdmin.user]); @@ -81,9 +100,25 @@ const setupData = async () => { model.name, model.properties, )); + const containerProms = containers.map((subModel) => ServiceHelper.db.createModel( + teamspace, + subModel._id, + subModel.name, + subModel.properties, + )); + const revisionsProms = containers.map((subModel) => ServiceHelper.db.createRevision( + teamspace, + project.id, + subModel._id, + conRevisions, + modelTypes.CONTAINER, + )); + return Promise.all([ ...userProms, ...modelProms, + ...containerProms, + ...revisionsProms, ServiceHelper.db.createUser(nobody), ServiceHelper.db.createProject(teamspace, project.id, project.name, models.map(({ _id }) => _id)), ]); @@ -181,44 +216,61 @@ const testNewRevision = () => { }; const testGetFederationMD5Hash = () => { - const route = ( - teamspace, - projectId = project.id, - model = models[0]._id, - revision = ServiceHelper.generateUUIDString(), - ) => `/v5/teamspaces/${teamspace}/projects/${projectId}/federations/${model}/revisions/${revision}/files/original/info`; describe('Get Federation MD5 Files', () => { - test('should fail without a valid session', async () => { - const res = await agent.get(route()).expect(templates.notLoggedIn.status); - expect(res.body.code).toEqual(templates.notLoggedIn.code); - }); + const generateTestData = () => { + const parameters = { + ts: teamspace, + projectId: project.id, + modelId: models[1]._id, + revisionId: ServiceHelper.generateUUIDString(), + key: users.tsAdmin.apiKey, + response: [], + }; + const viewerResponse = [{ + container: containers[0]._id, + code: conRevisions._id, + uploadedAt: new Date(conRevisions.timestamp).getTime(), + hash: CryptoJs.MD5(Buffer.from(conRevisions.rFile[0])).toString(), + filename: conRevisions.rFile[0], + size: 20, + }]; + const adminResponse = containers.map((model) => ({ + container: model._id, + code: conRevisions._id, + uploadedAt: new Date(conRevisions.timestamp).getTime(), + hash: CryptoJs.MD5(Buffer.from(conRevisions.rFile[0])).toString(), + filename: conRevisions.rFile[0], + size: 20, + })); - test('should fail if the user is not a member of the teamspace', async () => { - const res = await agent.get(`${route()}?key=${nobody.apiKey}`).expect(templates.teamspaceNotFound.status); - expect(res.body.code).toEqual(templates.teamspaceNotFound.code); - }); + // ask about the empty array problem + return [ + ['there is no valid session key but return an empty array.', { ...parameters, key: null }, true], + ['the user is not a member of the teamspace but return an empty array.', { ...parameters, key: nobody.apiKey }, true], + ['the user does not have access to the project but return an empty array.', { ...parameters, key: users.noProjectAccess.apiKey }, true], + ['the teamspace does not exist but return an empty array.', { ...parameters, ts: ServiceHelper.generateUUIDString() }, false, templates.federationNotFound], + ['the federation does not exist but return an empty array.', { ...parameters, modelId: ServiceHelper.generateUUIDString() }, false, templates.federationNotFound], + ['the viewer access it and return just that information.', { ...parameters, key: users.viewer.apiKey, response: viewerResponse }, true], + ['the admin access it and return all the information.', { ...parameters, response: adminResponse }, true], + ]; + }; - test('should fail if the user does not have access to the project', async () => { - const res = await agent.get(`${route()}?key=${users.noProjectAccess.apiKey}`).expect(templates.notAuthorized.status); - expect(res.body.code).toEqual(templates.notAuthorized.code); - }); + const runTest = (description, parameters, success, error) => { + const route = ({ ts, projectId, modelId, revisionId, key }) => `/v5/teamspaces/${ts}/projects/${projectId}/federations/${modelId}/revisions/${revisionId}/files/original/info${key ? `?key=${key}` : ''}`; - test('should fail if the project does not exist', async () => { - const res = await agent.get(`${route(teamspace, 'nelskfjdlsf')}?key=${users.tsAdmin.apiKey}`) - .expect(templates.projectNotFound.status); - expect(res.body.code).toEqual(templates.projectNotFound.code); - }); + test(`should ${success ? 'succeed' : `fail with ${error.code}`} if ${description}`, async () => { + const expectedStatus = success ? templates.ok.status : error.status; + const res = await agent.get(`${route(parameters)}`).expect(expectedStatus); - test('should fail if the federation does not exist', async () => { - const res = await agent.get(`${route(teamspace, project.id, 'sdlfkds')}?key=${users.tsAdmin.apiKey}`) - .expect(templates.federationNotFound.status); - expect(res.body.code).toEqual(templates.federationNotFound.code); - }); + if (success) { + expect(JSON.parse(res.text)).toEqual(parameters.response); + } else { + expect(res.body.code).toEqual(error.code); + } + }); + }; - test('should succeed if correct parameters are sent', async () => { - await agent.get(`${route()}?key=${users.tsAdmin.apiKey}`) - .expect(templates.ok.status); - }); + describe.each(generateTestData())('Federations', runTest); }); }; @@ -232,6 +284,7 @@ describe(ServiceHelper.determineTestGroup(__filename), () => { ServiceHelper.queue.purgeQueues(), ServiceHelper.closeApp(server), ])); + testNewRevision(); testGetFederationMD5Hash(); }); diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js index bf09b236076..bd23f861ef7 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js @@ -372,7 +372,7 @@ const testGetMD5Hash = () => { const revisionMock = { _id: Buffer.from('testBuffer'), rFile: ['success!'], timestamp: new Date() }; const fileEntry = { size: 100, type: 'fs', link: generateRandomString() }; const mockConatiners = [{ _id: '1', name: 'test1', permissions: [{ user: 'user1' }] }, { _id: '2', name: 'test2', permissions: [] }, { _id: '3', name: 'test3' }]; - ModelSettings.getContainers.mockImplementation((teamspace, containers, params) => { + ModelSettings.getContainers.mockImplementation((teamspace, containers) => { if (containers.length > 1) { return mockConatiners; } From 246fbb97e6644f1d2258386a34d3c72f775f15d8 Mon Sep 17 00:00:00 2001 From: Bogdan Cornea Date: Thu, 2 Jan 2025 13:57:39 +0000 Subject: [PATCH 04/15] tidy up --- backend/src/v5/models/fileRefs.js | 6 +++ .../projects/models/commons/modelList.js | 43 +++++++++++++------ backend/tests/v5/unit/models/fileRefs.test.js | 19 ++++++++ .../projects/models/containers.test.js | 30 +++++++++++-- 4 files changed, 82 insertions(+), 16 deletions(-) diff --git a/backend/src/v5/models/fileRefs.js b/backend/src/v5/models/fileRefs.js index 21a0b537a87..8459455013d 100644 --- a/backend/src/v5/models/fileRefs.js +++ b/backend/src/v5/models/fileRefs.js @@ -82,4 +82,10 @@ FileRefs.removeRefsByQuery = (teamspace, collection, query) => db.deleteMany( teamspace, collectionName(collection), query, ); +FileRefs.updateRef = async (teamspace, collection, query, action) => { + await db.updateOne( + teamspace, collectionName(collection), query, action, + ); +}; + module.exports = FileRefs; diff --git a/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js b/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js index d569db1f056..949895175c6 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js +++ b/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js @@ -18,13 +18,13 @@ const { addModel, getContainers } = require('../../../../../models/modelSettings'); const { addModelToProject, getProjectById, removeModelFromProject } = require('../../../../../models/projectSettings'); const { getLatestRevision, getRevisionByIdOrTag } = require('../../../../../models/revisions'); +const { getRefEntry, updateRef } = require('../../../../../models/fileRefs'); const { hasProjectAdminPermissions, isTeamspaceAdmin } = require('../../../../../utils/permissions/permissions'); const CryptoJs = require('crypto-js'); const { USERS_DB_NAME } = require('../../../../../models/users.constants'); const UUIDParse = require('uuid-parse'); const { getFavourites } = require('../../../../../models/users'); const { getFile } = require('../../../../../services/filesManager'); -const { getRefEntry } = require('../../../../../models/fileRefs'); const { modelTypes } = require('../../../../../models/modelSettings.constants'); const { removeModelData } = require('../../../../../utils/helper/models'); @@ -91,23 +91,40 @@ ModelList.getModelMD5Hash = async (teamspace, container, revision, user) => { // check if anything is in there if (!rev.rFile?.length) return returnValue; + const code = UUIDParse.unparse(rev._id.buffer); + const uploadedAt = new Date(rev.timestamp).getTime(); const filename = rev.rFile[0]; - const file = await getFile(teamspace, `${container}.history`, filename); + // check if the ref has the MD5 hash const refEntry = await getRefEntry(teamspace, `${container}.history.ref`, filename); - const hash = CryptoJs.MD5(file).toString(); - const code = UUIDParse.unparse(rev._id.buffer); - const uploadedAt = new Date(rev.timestamp).getTime(); + if (Object.keys(refEntry).includes('MD5Hash')) { + // if the ref has the hash create the object + returnValue = { + container, + code, + uploadedAt, + hash: refEntry.MD5Hash, + filename, + size: refEntry.size, + }; + } else { + // if the ref does not have the hash get the file, create the hash, set the return object and update the ref with the hash + const file = await getFile(teamspace, `${container}.history`, filename); + + const hash = CryptoJs.MD5(file).toString(); - returnValue = { - container, - code, - uploadedAt, - hash, - filename, - size: refEntry.size, - }; + returnValue = { + container, + code, + uploadedAt, + hash, + filename, + size: refEntry.size, + }; + + await updateRef(teamspace, `${container}.history.ref`, { _id: filename }, { $set: { MD5Hash: hash } }); + } return returnValue; }; diff --git a/backend/tests/v5/unit/models/fileRefs.test.js b/backend/tests/v5/unit/models/fileRefs.test.js index 49c0a83c0ee..078cf989ff0 100644 --- a/backend/tests/v5/unit/models/fileRefs.test.js +++ b/backend/tests/v5/unit/models/fileRefs.test.js @@ -200,6 +200,24 @@ const testRemoveRefsByQuery = () => { }); }; +const testUpdateRef = () => { + describe('Update the ref by query', () => { + test('should update file ref\'s', async () => { + const teamspace = generateRandomString(); + const collection = `${generateRandomString()}.history.ref`; + const query = { _id: generateRandomString() }; + const action = { $set: { [generateRandomString()]: generateRandomString() } }; + + const fn = jest.spyOn(db, 'updateOne').mockResolvedValueOnce(undefined); + + await expect(FileRefs.updateRef(teamspace, collection, query, action)).resolves.toEqual(undefined); + + expect(fn).toHaveBeenCalledTimes(1); + expect(fn).toHaveBeenCalledWith(teamspace, collection, query, action); + }); + }); +}; + describe('models/fileRefs', () => { testGetTotalSize(); testGetAllRemovableEntriesByType(); @@ -209,4 +227,5 @@ describe('models/fileRefs', () => { testRemoveRef(); testGetRefsByQuery(); testRemoveRefsByQuery(); + testUpdateRef(); }); diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js index 11aa4573a45..4a74fa55fed 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js @@ -524,7 +524,7 @@ const testGetMD5Hash = () => { // given: Revisions.getRevisionByIdOrTag.mockResolvedValueOnce(templates.revisionNotFound); - // it shoul + // it should await expect(Containers.getRevisionMD5Hash('teamspace', 'container', generateUUIDString(), 'user1')).resolves.toEqual(); expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); @@ -534,14 +534,14 @@ const testGetMD5Hash = () => { test('should return empty if user does not have access to the revision', async () => { // given - // it shuld + // it should await expect(Containers.getRevisionMD5Hash('teamspace', 'container', generateUUIDString(), 'NoAccess')).resolves.toEqual(); expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); expect(FilesManager.getFile).toHaveBeenCalledTimes(0); }); - test('should return an object if revision has file', async () => { + test('should return an object if revision has a valid file and the file should be retrieved if no MD5Hash exists in the fileRef', async () => { // given: const revisionMock = { _id: Buffer.from('testBuffer'), rFile: ['success!'], timestamp: new Date() }; const revisionCodeMock = generateUUIDString(); @@ -565,6 +565,30 @@ const testGetMD5Hash = () => { expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(1); expect(FilesManager.getFile).toHaveBeenCalledTimes(1); }); + test('should return an object if revision has has a valid file and the file should not be retrieved if MD5Hash exists in the fileRef', async () => { + // given: + const revisionMock = { _id: Buffer.from('testBuffer'), rFile: ['success!'], timestamp: new Date() }; + const revisionCodeMock = generateUUIDString(); + const fileEntry = { size: 100, type: 'fs', link: generateRandomString(), MD5Hash: CryptoJs.MD5(revisionMock._id).toString() }; + + Revisions.getRevisionByIdOrTag.mockResolvedValueOnce(revisionMock); + FilesManager.getFile.mockResolvedValueOnce(revisionMock._id); + FileRefs.getRefEntry.mockResolvedValueOnce(fileEntry); + + // it should + await expect(Containers.getRevisionMD5Hash('teamspace', 'container', revisionCodeMock, 'user1')).resolves.toEqual({ + container: 'container', + code: UUIDParse.unparse(revisionMock._id.buffer), + uploadedAt: new Date(revisionMock.timestamp).getTime(), + hash: CryptoJs.MD5(revisionMock._id).toString(), + filename: revisionMock.rFile[0], + size: fileEntry.size, + }); + + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); + expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(1); + expect(FilesManager.getFile).toHaveBeenCalledTimes(0); + }); }); }; From 71350c91b478092374ce48e3a0cbca6c60d961a9 Mon Sep 17 00:00:00 2001 From: Bogdan Cornea Date: Thu, 2 Jan 2025 17:08:30 +0000 Subject: [PATCH 05/15] permission fix --- .../projects/models/federations/revisions.js | 3 +- .../models/federations/revisions.test.js | 103 +++++++++--------- 2 files changed, 51 insertions(+), 55 deletions(-) diff --git a/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js b/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js index cef21c404ec..ae9356d1d8b 100644 --- a/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js +++ b/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js @@ -15,11 +15,10 @@ * along with this program. If not, see . */ +const { hasReadAccessToFederation, hasWriteAccessToFederation } = require('../../../../../middleware/permissions/permissions'); const Federations = require('../../../../../processors/teamspaces/projects/models/federations'); const { Router } = require('express'); const { getUserFromSession } = require('../../../../../utils/sessions'); -const { hasReadAccessToFederation } = require('../../../../../utils/permissions/permissions'); -const { hasWriteAccessToFederation } = require('../../../../../middleware/permissions/permissions'); const { respond } = require('../../../../../utils/responder'); const { templates } = require('../../../../../utils/responseCodes'); const { validateNewRevisionData } = require('../../../../../middleware/dataConverter/inputs/teamspaces/projects/models/federations'); diff --git a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js index bc5e9142ce5..9f094ca9c11 100644 --- a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js +++ b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js @@ -62,7 +62,7 @@ const models = [ name: ServiceHelper.generateRandomString(), properties: { ...ServiceHelper.generateRandomModelProperties(), - permissions: [{ user: users.viewer, permission: 'viewer' }, { user: users.commenter, permission: 'commenter' }], + permissions: [{ user: users.viewer.user, permission: 'viewer' }, { user: users.commenter, permission: 'commenter' }], federate: true, subModels: containers.map((model) => ({ _id: model._id })), }, @@ -72,7 +72,7 @@ const models = [ name: ServiceHelper.generateRandomString(), properties: { ...ServiceHelper.generateRandomModelProperties(modelTypes.FEDERATION), - permissions: [{ user: users.viewer, permission: 'viewer' }, { user: users.commenter, permission: 'commenter' }], + permissions: [{ user: users.viewer.user, permission: 'viewer' }, { user: users.commenter, permission: 'commenter' }], federate: true, subModels: containers.map((model) => ({ _id: model._id })), }, @@ -216,62 +216,59 @@ const testNewRevision = () => { }; const testGetFederationMD5Hash = () => { - describe('Get Federation MD5 Files', () => { - const generateTestData = () => { - const parameters = { - ts: teamspace, - projectId: project.id, - modelId: models[1]._id, - revisionId: ServiceHelper.generateUUIDString(), - key: users.tsAdmin.apiKey, - response: [], - }; - const viewerResponse = [{ - container: containers[0]._id, - code: conRevisions._id, - uploadedAt: new Date(conRevisions.timestamp).getTime(), - hash: CryptoJs.MD5(Buffer.from(conRevisions.rFile[0])).toString(), - filename: conRevisions.rFile[0], - size: 20, - }]; - const adminResponse = containers.map((model) => ({ - container: model._id, - code: conRevisions._id, - uploadedAt: new Date(conRevisions.timestamp).getTime(), - hash: CryptoJs.MD5(Buffer.from(conRevisions.rFile[0])).toString(), - filename: conRevisions.rFile[0], - size: 20, - })); - - // ask about the empty array problem - return [ - ['there is no valid session key but return an empty array.', { ...parameters, key: null }, true], - ['the user is not a member of the teamspace but return an empty array.', { ...parameters, key: nobody.apiKey }, true], - ['the user does not have access to the project but return an empty array.', { ...parameters, key: users.noProjectAccess.apiKey }, true], - ['the teamspace does not exist but return an empty array.', { ...parameters, ts: ServiceHelper.generateUUIDString() }, false, templates.federationNotFound], - ['the federation does not exist but return an empty array.', { ...parameters, modelId: ServiceHelper.generateUUIDString() }, false, templates.federationNotFound], - ['the viewer access it and return just that information.', { ...parameters, key: users.viewer.apiKey, response: viewerResponse }, true], - ['the admin access it and return all the information.', { ...parameters, response: adminResponse }, true], - ]; + const generateTestData = () => { + const parameters = { + ts: teamspace, + projectId: project.id, + modelId: models[1]._id, + revisionId: ServiceHelper.generateUUIDString(), + key: users.tsAdmin.apiKey, + response: [], }; + const viewerResponse = [{ + container: containers[0]._id, + code: conRevisions._id, + uploadedAt: new Date(conRevisions.timestamp).getTime(), + hash: CryptoJs.MD5(Buffer.from(conRevisions.rFile[0])).toString(), + filename: conRevisions.rFile[0], + size: 20, + }]; + const adminResponse = containers.map((model) => ({ + container: model._id, + code: conRevisions._id, + uploadedAt: new Date(conRevisions.timestamp).getTime(), + hash: CryptoJs.MD5(Buffer.from(conRevisions.rFile[0])).toString(), + filename: conRevisions.rFile[0], + size: 20, + })); - const runTest = (description, parameters, success, error) => { - const route = ({ ts, projectId, modelId, revisionId, key }) => `/v5/teamspaces/${ts}/projects/${projectId}/federations/${modelId}/revisions/${revisionId}/files/original/info${key ? `?key=${key}` : ''}`; + return [ + ['there is no valid session key but return an empty array.', { ...parameters, key: null }, false, templates.notLoggedIn], + ['the user is not a member of the teamspace but return an empty array.', { ...parameters, key: nobody.apiKey }, false, templates.teamspaceNotFound], + ['the user does not have access to the project but return an empty array.', { ...parameters, key: users.noProjectAccess.apiKey }, false, templates.notAuthorized], + ['the teamspace does not exist but return an empty array.', { ...parameters, ts: ServiceHelper.generateUUIDString() }, false, templates.teamspaceNotFound], + ['the federation does not exist but return an empty array.', { ...parameters, modelId: ServiceHelper.generateUUIDString() }, false, templates.federationNotFound], + ['the viewer access it and return just that information.', { ...parameters, key: users.viewer.apiKey, response: viewerResponse }, true], + ['the admin access it and return all the information.', { ...parameters, response: adminResponse }, true], + ]; + }; - test(`should ${success ? 'succeed' : `fail with ${error.code}`} if ${description}`, async () => { - const expectedStatus = success ? templates.ok.status : error.status; - const res = await agent.get(`${route(parameters)}`).expect(expectedStatus); + const runTest = (description, parameters, success, error) => { + const route = ({ ts, projectId, modelId, revisionId, key }) => `/v5/teamspaces/${ts}/projects/${projectId}/federations/${modelId}/revisions/${revisionId}/files/original/info${key ? `?key=${key}` : ''}`; - if (success) { - expect(JSON.parse(res.text)).toEqual(parameters.response); - } else { - expect(res.body.code).toEqual(error.code); - } - }); - }; + test(`should ${success ? 'succeed' : `fail with ${error.code}`} if ${description}`, async () => { + const expectedStatus = success ? templates.ok.status : error.status; + const res = await agent.get(`${route(parameters)}`).expect(expectedStatus); - describe.each(generateTestData())('Federations', runTest); - }); + if (success) { + expect(JSON.parse(res.text)).toEqual(parameters.response); + } else { + expect(res.body.code).toEqual(error.code); + } + }); + }; + + describe.each(generateTestData())('Get Federation MD5 Files', runTest); }; describe(ServiceHelper.determineTestGroup(__filename), () => { From 91888826a653c934dcfb8451a5ce795963f7acc7 Mon Sep 17 00:00:00 2001 From: Bogdan Cornea Date: Tue, 7 Jan 2025 11:34:59 +0000 Subject: [PATCH 06/15] update federations test for out of order arrays --- .../teamspaces/projects/models/federations.test.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js index bd23f861ef7..e88bc2fbe6d 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js @@ -16,7 +16,7 @@ */ const { src } = require('../../../../../helper/path'); -const { generateRandomString, generateRandomObject, determineTestGroup, generateRandomNumber } = require('../../../../../helper/services'); +const { generateRandomString, generateRandomObject, determineTestGroup, generateRandomNumber, outOfOrderArrayEqual } = require('../../../../../helper/services'); const UUIDParse = require('uuid-parse'); const CryptoJs = require('crypto-js'); @@ -423,9 +423,9 @@ const testGetMD5Hash = () => { FilesManager.getFile.mockImplementation(() => revisionMock._id); Revisions.getLatestRevision.mockResolvedValue(revisionMock); FilesRef.getRefEntry.mockResolvedValue(fileEntry); - + // it should - await expect(Federations.getMD5Hash('teamspace', 'federation', 'tsAdmin')).resolves.toEqual([ + outOfOrderArrayEqual(await Federations.getMD5Hash('teamspace', 'federation', 'tsAdmin'), [ { container: '1', code: UUIDParse.unparse(revisionMock._id.buffer), @@ -448,7 +448,6 @@ const testGetMD5Hash = () => { filename: revisionMock.rFile[0], size: fileEntry.size, }]); - expect(ModelSettings.getFederationById).toHaveBeenCalledTimes(1); expect(ModelSettings.getContainers).toHaveBeenCalledTimes(4); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); From f16fae053c3df21e18a5ea89e5b45e491fab9b05 Mon Sep 17 00:00:00 2001 From: Bogdan Cornea Date: Tue, 7 Jan 2025 11:54:11 +0000 Subject: [PATCH 07/15] update the e2e with out of order array check --- .../teamspaces/projects/models/federations/revisions.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js index 9f094ca9c11..588a3491296 100644 --- a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js +++ b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js @@ -19,6 +19,7 @@ const SuperTest = require('supertest'); const ServiceHelper = require('../../../../../../helper/services'); const { src } = require('../../../../../../helper/path'); const CryptoJs = require('crypto-js'); +const {outOfOrderArrayEqual} = require('../../../../../../helper/services') const { templates } = require(`${src}/utils/responseCodes`); const { modelTypes } = require(`${src}/models/modelSettings.constants`); @@ -261,7 +262,7 @@ const testGetFederationMD5Hash = () => { const res = await agent.get(`${route(parameters)}`).expect(expectedStatus); if (success) { - expect(JSON.parse(res.text)).toEqual(parameters.response); + outOfOrderArrayEqual(JSON.parse(res.text),parameters.response); } else { expect(res.body.code).toEqual(error.code); } From d3dfe0eeb04119290098aed9aa7b7268e189efe2 Mon Sep 17 00:00:00 2001 From: Bogdan Cornea Date: Tue, 7 Jan 2025 11:58:09 +0000 Subject: [PATCH 08/15] lint fixes --- .../teamspaces/projects/models/federations/revisions.test.js | 4 ++-- .../processors/teamspaces/projects/models/federations.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js index 588a3491296..8f6c982737f 100644 --- a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js +++ b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js @@ -19,7 +19,7 @@ const SuperTest = require('supertest'); const ServiceHelper = require('../../../../../../helper/services'); const { src } = require('../../../../../../helper/path'); const CryptoJs = require('crypto-js'); -const {outOfOrderArrayEqual} = require('../../../../../../helper/services') +const { outOfOrderArrayEqual } = require('../../../../../../helper/services'); const { templates } = require(`${src}/utils/responseCodes`); const { modelTypes } = require(`${src}/models/modelSettings.constants`); @@ -262,7 +262,7 @@ const testGetFederationMD5Hash = () => { const res = await agent.get(`${route(parameters)}`).expect(expectedStatus); if (success) { - outOfOrderArrayEqual(JSON.parse(res.text),parameters.response); + outOfOrderArrayEqual(JSON.parse(res.text), parameters.response); } else { expect(res.body.code).toEqual(error.code); } diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js index e88bc2fbe6d..5264d40aa69 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js @@ -423,7 +423,7 @@ const testGetMD5Hash = () => { FilesManager.getFile.mockImplementation(() => revisionMock._id); Revisions.getLatestRevision.mockResolvedValue(revisionMock); FilesRef.getRefEntry.mockResolvedValue(fileEntry); - + // it should outOfOrderArrayEqual(await Federations.getMD5Hash('teamspace', 'federation', 'tsAdmin'), [ { From 9d93e8314319e806af16a144663788b7dd1964b2 Mon Sep 17 00:00:00 2001 From: Bogdan Cornea Date: Wed, 8 Jan 2025 13:52:12 +0000 Subject: [PATCH 09/15] Update backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js ISSUE_5287 Co-authored-by: Carmen Fan --- .../processors/teamspaces/projects/models/commons/modelList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js b/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js index 949895175c6..9c81c8fb607 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js +++ b/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js @@ -77,7 +77,7 @@ ModelList.getModelMD5Hash = async (teamspace, container, revision, user) => { if (!isAdmin && !containers[0].permissions?.some((permission) => permission?.user === user)) return returnValue; // retrieve the right revision - if (revision?.length) { + if (revision) { rev = await getRevisionByIdOrTag( teamspace, container, modelTypes.CONTAINER, revision, { rFile: 1, timestamp: 1, fileSize: 1 }, From 874d261be7798f5f37297fcb5f832da726f10c6c Mon Sep 17 00:00:00 2001 From: Bogdan Cornea Date: Tue, 14 Jan 2025 14:30:45 +0000 Subject: [PATCH 10/15] federation access issue ISSUE_5287 --- .../projects/models/commons/modelList.js | 70 +++++-------------- .../teamspaces/projects/models/containers.js | 5 +- .../teamspaces/projects/models/federations.js | 17 +++-- .../projects/models/common/revisions.js | 3 +- .../projects/models/federations/revisions.js | 4 +- backend/src/v5/services/filesManager.js | 23 ++++++ .../src/v5/utils/permissions/permissions.js | 1 - .../projects/models/common/revisions.test.js | 4 +- .../models/federations/revisions.test.js | 52 +++++++------- .../projects/models/containers.test.js | 57 +++------------ .../projects/models/federations.test.js | 56 +++++++-------- .../v5/unit/services/filesManager.test.js | 28 ++++++++ 12 files changed, 147 insertions(+), 173 deletions(-) diff --git a/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js b/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js index 9c81c8fb607..313f55f64dd 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js +++ b/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js @@ -15,16 +15,13 @@ * along with this program. If not, see . */ -const { addModel, getContainers } = require('../../../../../models/modelSettings'); const { addModelToProject, getProjectById, removeModelFromProject } = require('../../../../../models/projectSettings'); const { getLatestRevision, getRevisionByIdOrTag } = require('../../../../../models/revisions'); -const { getRefEntry, updateRef } = require('../../../../../models/fileRefs'); const { hasProjectAdminPermissions, isTeamspaceAdmin } = require('../../../../../utils/permissions/permissions'); -const CryptoJs = require('crypto-js'); const { USERS_DB_NAME } = require('../../../../../models/users.constants'); -const UUIDParse = require('uuid-parse'); +const { addModel } = require('../../../../../models/modelSettings'); const { getFavourites } = require('../../../../../models/users'); -const { getFile } = require('../../../../../services/filesManager'); +const { getMD5FileHash } = require('../../../../../services/filesManager'); const { modelTypes } = require('../../../../../models/modelSettings.constants'); const { removeModelData } = require('../../../../../utils/helper/models'); @@ -65,68 +62,35 @@ ModelList.getModelList = async (teamspace, project, user, modelSettings) => { }); }; -ModelList.getModelMD5Hash = async (teamspace, container, revision, user) => { - const [isAdmin, containers] = await Promise.all([ - isTeamspaceAdmin(teamspace, user), - getContainers(teamspace, [container], { _id: 1, name: 1, permissions: 1 }), - ]); +ModelList.getModelMD5Hash = async (teamspace, container, revision) => { let rev; - let returnValue; - - // if not allowed just return nothing - if (!isAdmin && !containers[0].permissions?.some((permission) => permission?.user === user)) return returnValue; - // retrieve the right revision if (revision) { rev = await getRevisionByIdOrTag( teamspace, container, modelTypes.CONTAINER, revision, - { rFile: 1, timestamp: 1, fileSize: 1 }, + { rFile: 1, timestamp: 1, fileSize: 1, tag: 1 }, { includeVoid: false }); } else { rev = await getLatestRevision( teamspace, container, modelTypes.CONTAINER, - { rFile: 1, timestamp: 1, fileSize: 1 }); + { rFile: 1, timestamp: 1, fileSize: 1, tag: 1 }); } - // check if anything is in there - if (!rev.rFile?.length) return returnValue; + if (!rev.rFile?.length) return {}; - const code = UUIDParse.unparse(rev._id.buffer); + const code = rev.tag; const uploadedAt = new Date(rev.timestamp).getTime(); const filename = rev.rFile[0]; - - // check if the ref has the MD5 hash - const refEntry = await getRefEntry(teamspace, `${container}.history.ref`, filename); - - if (Object.keys(refEntry).includes('MD5Hash')) { - // if the ref has the hash create the object - returnValue = { - container, - code, - uploadedAt, - hash: refEntry.MD5Hash, - filename, - size: refEntry.size, - }; - } else { - // if the ref does not have the hash get the file, create the hash, set the return object and update the ref with the hash - const file = await getFile(teamspace, `${container}.history`, filename); - - const hash = CryptoJs.MD5(file).toString(); - - returnValue = { - container, - code, - uploadedAt, - hash, - filename, - size: refEntry.size, - }; - - await updateRef(teamspace, `${container}.history.ref`, { _id: filename }, { $set: { MD5Hash: hash } }); - } - - return returnValue; + const { hash, size } = await getMD5FileHash(teamspace, container, filename); + + return { + container, + code, + uploadedAt, + hash, + filename, + size, + }; }; module.exports = ModelList; diff --git a/backend/src/v5/processors/teamspaces/projects/models/containers.js b/backend/src/v5/processors/teamspaces/projects/models/containers.js index 90aa83182f5..f1e1c869887 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/containers.js +++ b/backend/src/v5/processors/teamspaces/projects/models/containers.js @@ -133,9 +133,6 @@ Containers.updateSettings = updateModelSettings; Containers.getSettings = (teamspace, container) => getContainerById(teamspace, container, { corID: 0, account: 0, permissions: 0 }); -Containers.getRevisionMD5Hash = async (teamspace, container, revision, user) => { - const response = await getModelMD5Hash(teamspace, container, revision, user); - return response; -}; +Containers.getRevisionMD5Hash = getModelMD5Hash; module.exports = Containers; diff --git a/backend/src/v5/processors/teamspaces/projects/models/federations.js b/backend/src/v5/processors/teamspaces/projects/models/federations.js index 3dca9a1ed03..882a4231bad 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/federations.js +++ b/backend/src/v5/processors/teamspaces/projects/models/federations.js @@ -27,6 +27,7 @@ const { getLatestRevision } = require('../../../../models/revisions'); const { getModelMD5Hash } = require('./commons/modelList'); const { getOpenTicketsCount } = require('./commons/tickets'); const { getProjectById } = require('../../../../models/projectSettings'); +const { hasReadAccessToContainer } = require('../../../../utils/permissions/permissions'); const { modelTypes } = require('../../../../models/modelSettings.constants'); const { queueFederationUpdate } = require('../../../../services/modelProcessing'); @@ -108,7 +109,7 @@ Federations.updateSettings = updateModelSettings; Federations.getSettings = (teamspace, federation) => getFederationById(teamspace, federation, { corID: 0, account: 0, permissions: 0, subModels: 0, federate: 0 }); -Federations.getMD5Hash = async (teamspace, federation, user) => { +Federations.getMD5Hash = async (teamspace, project, federation, user) => { const { subModels: containers } = await getFederationById(teamspace, federation, { subModels: 1 }); const containerWithMetadata = await getContainers( teamspace, @@ -116,13 +117,17 @@ Federations.getMD5Hash = async (teamspace, federation, user) => { { _id: 1, name: 1, permissions: 1 }); const listOfPromises = containerWithMetadata.map( - (container) => getModelMD5Hash(teamspace, container._id, null, user)); + async (container) => { + const hasAccess = await hasReadAccessToContainer(teamspace, project, container._id, user); + if (hasAccess) { + return getModelMD5Hash(teamspace, container._id); + } + return undefined; + }, + ); const promiseResponses = await Promise.allSettled(listOfPromises); - const responses = promiseResponses - // make sure the promise is fulfilled and the value is not undefined or empty - .filter((response) => response.status === 'fulfilled' && response.value !== undefined) - .map((response) => response.value); + const responses = promiseResponses.flatMap(({ status, value }) => (status === 'fulfilled' && value ? value : [])); return responses; }; diff --git a/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js b/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js index 15e62dd4c61..3db87d70db2 100644 --- a/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js +++ b/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js @@ -61,10 +61,9 @@ const getRevisions = (modelType) => async (req, res, next) => { const getRevisionMD5Hash = () => async (req, res) => { const { teamspace, container, revision } = req.params; - const user = getUserFromSession(req.session); try { - const response = await Containers.getRevisionMD5Hash(teamspace, container, revision, user); + const response = await Containers.getRevisionMD5Hash(teamspace, container, revision); respond(req, res, templates.ok, response); } catch (err) { /* istanbul ignore next */ diff --git a/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js b/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js index ae9356d1d8b..924f0465727 100644 --- a/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js +++ b/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js @@ -37,11 +37,11 @@ const createNewFederationRevision = async (req, res) => { }; const getFederationMD5Hash = async (req, res) => { - const { teamspace, federation } = req.params; + const { teamspace, project, federation } = req.params; const user = getUserFromSession(req.session); try { - const response = await Federations.getMD5Hash(teamspace, federation, user); + const response = await Federations.getMD5Hash(teamspace, project, federation, user); respond(req, res, templates.ok, response); } catch (err) { /* istanbul ignore next */ diff --git a/backend/src/v5/services/filesManager.js b/backend/src/v5/services/filesManager.js index 0ed31966727..79e0f12b068 100644 --- a/backend/src/v5/services/filesManager.js +++ b/backend/src/v5/services/filesManager.js @@ -23,7 +23,9 @@ const { insertManyRefs, insertRef, removeRefsByQuery, + updateRef, } = require('../models/fileRefs'); +const CryptoJs = require('crypto-js'); const FSHandler = require('../handler/fs'); const GridFSHandler = require('../handler/gridfs'); const config = require('../utils/config'); @@ -214,4 +216,25 @@ FilesManager.storeFileStream = async (teamspace, collection, id, dataStream, met await insertRef(teamspace, collection, { ...meta, ...refInfo, _id: id, mimeType }); }; +FilesManager.getMD5FileHash = async (teamspace, container, filename) => { + const refEntry = await getRefEntry(teamspace, `${container}.history.ref`, filename); + + if (Object.keys(refEntry).includes('MD5Hash')) { + return { + hash: refEntry.MD5Hash, + size: refEntry.size, + }; + } + + const file = await FilesManager.getFile(teamspace, `${container}.history`, filename); + const hash = CryptoJs.MD5(file).toString(); + + await updateRef(teamspace, `${container}.history.ref`, { _id: filename }, { $set: { MD5Hash: hash } }); + + return { + hash, + size: refEntry.size, + }; +}; + module.exports = FilesManager; diff --git a/backend/src/v5/utils/permissions/permissions.js b/backend/src/v5/utils/permissions/permissions.js index 350c9eb4710..c3379d4247b 100644 --- a/backend/src/v5/utils/permissions/permissions.js +++ b/backend/src/v5/utils/permissions/permissions.js @@ -65,7 +65,6 @@ const modelPermCheck = (permCheck, modelType) => async (teamspace, project, mode } const model = await getModelFn(teamspace, modelID, { permissions: 1 }); - const modelExists = await modelsExistInProject(teamspace, project, [modelID]); if (!modelExists) { return false; diff --git a/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js b/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js index 2a1fa5b94ba..7f7e77edd25 100644 --- a/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js +++ b/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js @@ -515,7 +515,7 @@ const testGetRevisionMD5Hash = () => { const MD5HashResponseExpectation = { container: model._id, - code: revision._id, + code: revision.tag, uploadedAt: new Date(revision.timestamp).getTime(), hash: CryptoJs.MD5(Buffer.from(revision.rFile[0])).toString(), filename: revision.rFile[0], @@ -532,7 +532,7 @@ const testGetRevisionMD5Hash = () => { return [ ['the user does not have a valid session.', { ...params, key: null }, false, templates.notLoggedIn], - ['the teamspace does not exist.', { ...params, ts: 'not a valid ts' }, false, templates.teamspaceNotFound], + ['the teamspace does not exist.', { ...params, ts: ServiceHelper.generateRandomString() }, false, templates.teamspaceNotFound], ['the user is not a member of the teamspace.', { ...params, key: users.nobody.apiKey }, false, templates.teamspaceNotFound], ['the user does not have access to the model.', { ...params, key: users.noProjectAccess.apiKey }, false, templates.notAuthorized], ['the user is viewer.', { ...params, key: users.viewer.apiKey }, true], diff --git a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js index 8f6c982737f..2ea3d937c03 100644 --- a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js +++ b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js @@ -42,20 +42,24 @@ const project = { id: ServiceHelper.generateUUIDString(), name: ServiceHelper.generateRandomString(), }; -const containers = [{ - _id: ServiceHelper.generateUUIDString(), - name: ServiceHelper.generateRandomString(), - properties: { - ...ServiceHelper.generateRandomModelProperties(modelTypes.CONTAINER), - permissions: [{ user: users.viewer.user, permission: 'viewer' }, { user: users.commenter.user, permission: 'commenter' }], + +const containers = [ + { + _id: ServiceHelper.generateUUIDString(), + name: ServiceHelper.generateRandomString(), + properties: { + ...ServiceHelper.generateRandomModelProperties(modelTypes.CONTAINER), + permissions: [{ user: users.viewer.user, permission: 'viewer' }, { user: users.commenter.user, permission: 'commenter' }], + }, }, -}, { - _id: ServiceHelper.generateUUIDString(), - name: ServiceHelper.generateRandomString(), - properties: { - ...ServiceHelper.generateRandomModelProperties(modelTypes.CONTAINER), + { + _id: ServiceHelper.generateUUIDString(), + name: ServiceHelper.generateRandomString(), + properties: { + ...ServiceHelper.generateRandomModelProperties(modelTypes.CONTAINER), + }, }, -}]; +]; const models = [ { @@ -63,7 +67,7 @@ const models = [ name: ServiceHelper.generateRandomString(), properties: { ...ServiceHelper.generateRandomModelProperties(), - permissions: [{ user: users.viewer.user, permission: 'viewer' }, { user: users.commenter, permission: 'commenter' }], + permissions: [{ user: users.viewer.user, permission: 'viewer' }, { user: users.commenter.user, permission: 'commenter' }], federate: true, subModels: containers.map((model) => ({ _id: model._id })), }, @@ -73,7 +77,7 @@ const models = [ name: ServiceHelper.generateRandomString(), properties: { ...ServiceHelper.generateRandomModelProperties(modelTypes.FEDERATION), - permissions: [{ user: users.viewer.user, permission: 'viewer' }, { user: users.commenter, permission: 'commenter' }], + permissions: [{ user: users.viewer.user, permission: 'viewer' }, { user: users.commenter.user, permission: 'commenter' }], federate: true, subModels: containers.map((model) => ({ _id: model._id })), }, @@ -120,8 +124,8 @@ const setupData = async () => { ...modelProms, ...containerProms, ...revisionsProms, - ServiceHelper.db.createUser(nobody), ServiceHelper.db.createProject(teamspace, project.id, project.name, models.map(({ _id }) => _id)), + ServiceHelper.db.createUser(nobody), ]); }; @@ -221,14 +225,14 @@ const testGetFederationMD5Hash = () => { const parameters = { ts: teamspace, projectId: project.id, - modelId: models[1]._id, + modelId: models[0]._id, revisionId: ServiceHelper.generateUUIDString(), key: users.tsAdmin.apiKey, response: [], }; const viewerResponse = [{ container: containers[0]._id, - code: conRevisions._id, + code: conRevisions.tag, uploadedAt: new Date(conRevisions.timestamp).getTime(), hash: CryptoJs.MD5(Buffer.from(conRevisions.rFile[0])).toString(), filename: conRevisions.rFile[0], @@ -236,7 +240,7 @@ const testGetFederationMD5Hash = () => { }]; const adminResponse = containers.map((model) => ({ container: model._id, - code: conRevisions._id, + code: conRevisions.tag, uploadedAt: new Date(conRevisions.timestamp).getTime(), hash: CryptoJs.MD5(Buffer.from(conRevisions.rFile[0])).toString(), filename: conRevisions.rFile[0], @@ -244,11 +248,11 @@ const testGetFederationMD5Hash = () => { })); return [ - ['there is no valid session key but return an empty array.', { ...parameters, key: null }, false, templates.notLoggedIn], - ['the user is not a member of the teamspace but return an empty array.', { ...parameters, key: nobody.apiKey }, false, templates.teamspaceNotFound], - ['the user does not have access to the project but return an empty array.', { ...parameters, key: users.noProjectAccess.apiKey }, false, templates.notAuthorized], - ['the teamspace does not exist but return an empty array.', { ...parameters, ts: ServiceHelper.generateUUIDString() }, false, templates.teamspaceNotFound], - ['the federation does not exist but return an empty array.', { ...parameters, modelId: ServiceHelper.generateUUIDString() }, false, templates.federationNotFound], + // ['there is no valid session key.', { ...parameters, key: null }, false, templates.notLoggedIn], + // ['the user is not a member of the teamspace.', { ...parameters, key: nobody.apiKey }, false, templates.teamspaceNotFound], + // ['the user does not have access to the project.', { ...parameters, key: users.noProjectAccess.apiKey }, false, templates.notAuthorized], + // ['the teamspace does not exist.', { ...parameters, ts: ServiceHelper.generateUUIDString() }, false, templates.teamspaceNotFound], + // ['the federation does not exist.', { ...parameters, modelId: ServiceHelper.generateUUIDString() }, false, templates.federationNotFound], ['the viewer access it and return just that information.', { ...parameters, key: users.viewer.apiKey, response: viewerResponse }, true], ['the admin access it and return all the information.', { ...parameters, response: adminResponse }, true], ]; @@ -283,6 +287,6 @@ describe(ServiceHelper.determineTestGroup(__filename), () => { ServiceHelper.closeApp(server), ])); - testNewRevision(); + // testNewRevision(); testGetFederationMD5Hash(); }); diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js index 4a74fa55fed..71e057af68d 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js @@ -525,69 +525,32 @@ const testGetMD5Hash = () => { Revisions.getRevisionByIdOrTag.mockResolvedValueOnce(templates.revisionNotFound); // it should - await expect(Containers.getRevisionMD5Hash('teamspace', 'container', generateUUIDString(), 'user1')).resolves.toEqual(); + await expect(Containers.getRevisionMD5Hash('teamspace', 'container', generateUUIDString())).resolves.toEqual({}); - expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(1); - expect(FilesManager.getFile).toHaveBeenCalledTimes(0); - }); - test('should return empty if user does not have access to the revision', async () => { - // given - - // it should - await expect(Containers.getRevisionMD5Hash('teamspace', 'container', generateUUIDString(), 'NoAccess')).resolves.toEqual(); - - expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); - expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); - expect(FilesManager.getFile).toHaveBeenCalledTimes(0); + expect(FilesManager.getMD5FileHash).toHaveBeenCalledTimes(0); }); test('should return an object if revision has a valid file and the file should be retrieved if no MD5Hash exists in the fileRef', async () => { // given: - const revisionMock = { _id: Buffer.from('testBuffer'), rFile: ['success!'], timestamp: new Date() }; - const revisionCodeMock = generateUUIDString(); - const fileEntry = { size: 100, type: 'fs', link: generateRandomString() }; - - Revisions.getRevisionByIdOrTag.mockResolvedValueOnce(revisionMock); - FilesManager.getFile.mockResolvedValueOnce(revisionMock._id); - FileRefs.getRefEntry.mockResolvedValueOnce(fileEntry); - - // it should - await expect(Containers.getRevisionMD5Hash('teamspace', 'container', revisionCodeMock, 'user1')).resolves.toEqual({ - container: 'container', - code: UUIDParse.unparse(revisionMock._id.buffer), - uploadedAt: new Date(revisionMock.timestamp).getTime(), - hash: CryptoJs.MD5(revisionMock._id).toString(), - filename: revisionMock.rFile[0], - size: fileEntry.size, - }); - - expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); - expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(1); - expect(FilesManager.getFile).toHaveBeenCalledTimes(1); - }); - test('should return an object if revision has has a valid file and the file should not be retrieved if MD5Hash exists in the fileRef', async () => { - // given: - const revisionMock = { _id: Buffer.from('testBuffer'), rFile: ['success!'], timestamp: new Date() }; + const revisionMock = { _id: generateRandomString(), rFile: ['success!'], timestamp: new Date(), tag: 'testTag' }; const revisionCodeMock = generateUUIDString(); - const fileEntry = { size: 100, type: 'fs', link: generateRandomString(), MD5Hash: CryptoJs.MD5(revisionMock._id).toString() }; + const fileHash = { hash: CryptoJs.MD5(revisionMock._id).toString(), size: 100 }; Revisions.getRevisionByIdOrTag.mockResolvedValueOnce(revisionMock); - FilesManager.getFile.mockResolvedValueOnce(revisionMock._id); - FileRefs.getRefEntry.mockResolvedValueOnce(fileEntry); + FilesManager.getMD5FileHash.mockResolvedValueOnce(fileHash); // it should - await expect(Containers.getRevisionMD5Hash('teamspace', 'container', revisionCodeMock, 'user1')).resolves.toEqual({ + await expect(Containers.getRevisionMD5Hash('teamspace', 'container', revisionCodeMock)).resolves.toEqual({ container: 'container', - code: UUIDParse.unparse(revisionMock._id.buffer), + code: revisionMock.tag, uploadedAt: new Date(revisionMock.timestamp).getTime(), - hash: CryptoJs.MD5(revisionMock._id).toString(), + hash: fileHash.hash, filename: revisionMock.rFile[0], - size: fileEntry.size, + size: fileHash.size, }); - expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(1); - expect(FilesManager.getFile).toHaveBeenCalledTimes(0); + expect(FilesManager.getMD5FileHash).toHaveBeenCalledTimes(1); }); }); }; diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js index 5264d40aa69..0c3aaec4456 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js @@ -18,7 +18,6 @@ const { src } = require('../../../../../helper/path'); const { generateRandomString, generateRandomObject, determineTestGroup, generateRandomNumber, outOfOrderArrayEqual } = require('../../../../../helper/services'); -const UUIDParse = require('uuid-parse'); const CryptoJs = require('crypto-js'); jest.mock('../../../../../../../src/v5/models/projectSettings'); @@ -63,6 +62,7 @@ const federationList = [ { _id: 4, name: 'federation 4', permissions: [] }, { _id: 5, name: 'federation 5' }, ]; +const mockContainers = [{ _id: '1', name: 'test1', permissions: [{ user: 'user1' }] }, { _id: '2', name: 'test2', permissions: [] }, { _id: '3', name: 'test3' }]; const federationSettings = { federation1: { @@ -162,6 +162,9 @@ jest.mock('../../../../../../../src/v5/utils/permissions/permissions', () => ({ ...jest.requireActual('../../../../../../../src/v5/utils/permissions/permissions'), isTeamspaceAdmin: jest.fn().mockImplementation((teamspace, user) => user === 'tsAdmin'), hasProjectAdminPermissions: jest.fn().mockImplementation((perm, user) => user === 'projAdmin'), + hasReadAccessToContainer: jest.fn().mockImplementation((teamspace, project, modelID, username) => username === 'tsAdmin' + || username === 'projAdmin' || mockContainers.filter((element) => element._id === modelID)[0].permissions.some((element) => element.user === username), + ), })); const determineResults = (username) => federationList.flatMap(({ permissions, _id, name }) => { @@ -369,91 +372,80 @@ const testGetTicketGroupById = () => { const testGetMD5Hash = () => { describe('Get MD5 hashes for each container in the federation', () => { - const revisionMock = { _id: Buffer.from('testBuffer'), rFile: ['success!'], timestamp: new Date() }; + const revisionMock = { _id: Buffer.from('testBuffer'), rFile: ['success!'], timestamp: new Date(), tag: 'tag' }; const fileEntry = { size: 100, type: 'fs', link: generateRandomString() }; - const mockConatiners = [{ _id: '1', name: 'test1', permissions: [{ user: 'user1' }] }, { _id: '2', name: 'test2', permissions: [] }, { _id: '3', name: 'test3' }]; + const mockMD5Hash = { hash: CryptoJs.MD5(revisionMock._id).toString(), size: fileEntry.size }; ModelSettings.getContainers.mockImplementation((teamspace, containers) => { if (containers.length > 1) { - return mockConatiners; + return mockContainers; } - return mockConatiners.filter((container) => container._id === containers[0]); + return mockContainers.filter((container) => container._id === containers[0]); }); + ModelSettings.getContainerById.mockImplementation( + (ts, container, project) => mockContainers.filter((cntr) => cntr._id === container)); + ProjectSettings.modelsExistInProject.mockResolvedValue(true); - // it should get an empty array if user doesn't have rights to the container test('should get an empty array if user does not have rights to the container', async () => { - // given ModelSettings.getFederationById.mockResolvedValueOnce({ subModels: [{ _id: '1' }, { _id: '2' }, { _id: '3' }] }); - // it should await expect(Federations.getMD5Hash('teamspace', 'federation', 'NoAcessUser')).resolves.toEqual([]); - - expect(ModelSettings.getContainers).toHaveBeenCalledTimes(4); + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); expect(Revisions.getLatestRevision).toHaveBeenCalledTimes(0); }); - // it should return just the containers the user has access to + test('it should return just the containers users have access to', async () => { - // given ModelSettings.getFederationById.mockResolvedValueOnce({ subModels: [{ _id: '1' }, { _id: '2' }, { _id: '3' }] }); - FilesManager.getFile.mockImplementation(() => revisionMock._id); Revisions.getLatestRevision.mockResolvedValueOnce(revisionMock); - FilesRef.getRefEntry.mockResolvedValueOnce(fileEntry); + FilesManager.getMD5FileHash.mockResolvedValueOnce(mockMD5Hash); - // it should - await expect(Federations.getMD5Hash('teamspace', 'federation', 'user1')).resolves.toEqual([{ container: '1', - code: UUIDParse.unparse(revisionMock._id.buffer), + await expect(Federations.getMD5Hash('teamspace', 'project', 'federation', 'user1')).resolves.toEqual([{ container: '1', + code: revisionMock.tag, uploadedAt: new Date(revisionMock.timestamp).getTime(), hash: CryptoJs.MD5(revisionMock._id).toString(), filename: revisionMock.rFile[0], size: fileEntry.size }]); - expect(ModelSettings.getFederationById).toHaveBeenCalledTimes(1); - expect(ModelSettings.getContainers).toHaveBeenCalledTimes(4); + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); expect(Revisions.getLatestRevision).toHaveBeenCalledTimes(1); - expect(FilesManager.getFile).toHaveBeenCalledTimes(1); - expect(FilesRef.getRefEntry).toHaveBeenCalledTimes(1); + expect(FilesManager.getMD5FileHash).toHaveBeenCalledTimes(1); }); - // it should return an array with all the containers if admin test('it should return an array with all the containers if admin', async () => { - // given ModelSettings.getFederationById.mockResolvedValueOnce({ subModels: [{ _id: '1' }, { _id: '2' }, { _id: '3' }] }); - FilesManager.getFile.mockImplementation(() => revisionMock._id); Revisions.getLatestRevision.mockResolvedValue(revisionMock); + FilesManager.getMD5FileHash.mockResolvedValue(mockMD5Hash); FilesRef.getRefEntry.mockResolvedValue(fileEntry); - // it should - outOfOrderArrayEqual(await Federations.getMD5Hash('teamspace', 'federation', 'tsAdmin'), [ + outOfOrderArrayEqual(await Federations.getMD5Hash('teamspace', 'project', 'federation', 'tsAdmin'), [ { container: '1', - code: UUIDParse.unparse(revisionMock._id.buffer), + code: revisionMock.tag, uploadedAt: new Date(revisionMock.timestamp).getTime(), hash: CryptoJs.MD5(revisionMock._id).toString(), filename: revisionMock.rFile[0], size: fileEntry.size, }, { container: '2', - code: UUIDParse.unparse(revisionMock._id.buffer), + code: revisionMock.tag, uploadedAt: new Date(revisionMock.timestamp).getTime(), hash: CryptoJs.MD5(revisionMock._id).toString(), filename: revisionMock.rFile[0], size: fileEntry.size, }, { container: '3', - code: UUIDParse.unparse(revisionMock._id.buffer), + code: revisionMock.tag, uploadedAt: new Date(revisionMock.timestamp).getTime(), hash: CryptoJs.MD5(revisionMock._id).toString(), filename: revisionMock.rFile[0], size: fileEntry.size, }]); expect(ModelSettings.getFederationById).toHaveBeenCalledTimes(1); - expect(ModelSettings.getContainers).toHaveBeenCalledTimes(4); + expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); expect(Revisions.getLatestRevision).toHaveBeenCalledTimes(3); - expect(FilesManager.getFile).toHaveBeenCalledTimes(3); - expect(FilesRef.getRefEntry).toHaveBeenCalledTimes(3); }); }); }; diff --git a/backend/tests/v5/unit/services/filesManager.test.js b/backend/tests/v5/unit/services/filesManager.test.js index 43c8de67a68..2af2af5d74f 100644 --- a/backend/tests/v5/unit/services/filesManager.test.js +++ b/backend/tests/v5/unit/services/filesManager.test.js @@ -15,6 +15,7 @@ * along with this program. If not, see . */ +const CryptoJS = require('crypto-js'); const { times } = require('lodash'); const { src } = require('../../helper/path'); const config = require('../../../../src/v5/utils/config'); @@ -695,6 +696,32 @@ const testRemoveFilesWithMeta = () => { }); }; +const testGetMD5FileHash = () => { + describe('Get a file\'s MD5 hash', () => { + const mockId = Buffer.from('testBuffer'); + const mockRefEntry = { type: 'fs', link: 'mockLink', size: 100 }; + test('it should create the MD5 hash if it has not been chached', async () => { + FileRefs.getRefEntry.mockImplementation(() => mockRefEntry); + FSHandler.getFile.mockResolvedValueOnce(mockId); + + await expect(FilesManager.getMD5FileHash('teamspace', 'container', 'filename')).resolves.toEqual({ hash: CryptoJS.MD5(mockId).toString(), size: 100 }); + + expect(FileRefs.getRefEntry).toHaveBeenCalledTimes(2); + expect(FSHandler.getFile).toHaveBeenCalledTimes(1); + expect(FileRefs.updateRef).toHaveBeenCalledTimes(1); + }); + test('it should return straight away if the MD5 has has been cached', async () => { + FileRefs.getRefEntry.mockResolvedValueOnce({ ...mockRefEntry, MD5Hash: CryptoJS.MD5(mockId).toString() }); + + await expect(FilesManager.getMD5FileHash('teamspace', 'container', 'filename')).resolves.toEqual({ hash: CryptoJS.MD5(mockId).toString(), size: 100 }); + + expect(FileRefs.getRefEntry).toHaveBeenCalledTimes(1); + expect(FSHandler.getFile).toHaveBeenCalledTimes(0); + expect(FileRefs.updateRef).toHaveBeenCalledTimes(0); + }); + }); +}; + describe('services/filesManager', () => { testRemoveFilesWithMeta(); testRemoveAllFilesFromModel(); @@ -706,4 +733,5 @@ describe('services/filesManager', () => { testStoreFiles(); testStoreFileStream(); testFileExists(); + testGetMD5FileHash(); }); From fb261ad9efd6cb31f5456d0844cb2732062abd71 Mon Sep 17 00:00:00 2001 From: Bogdan Cornea Date: Tue, 14 Jan 2025 17:21:22 +0000 Subject: [PATCH 11/15] addressing comments #ISSUE_5287 --- .../teamspaces/projects/models/federations.js | 40 ++++++------ .../models/federations/revisions.test.js | 64 ++++++++++++++++--- .../projects/models/containers.test.js | 14 ++-- .../projects/models/federations.test.js | 22 +++++-- 4 files changed, 98 insertions(+), 42 deletions(-) diff --git a/backend/src/v5/processors/teamspaces/projects/models/federations.js b/backend/src/v5/processors/teamspaces/projects/models/federations.js index 882a4231bad..852752d7d88 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/federations.js +++ b/backend/src/v5/processors/teamspaces/projects/models/federations.js @@ -111,25 +111,29 @@ Federations.getSettings = (teamspace, federation) => getFederationById(teamspace Federations.getMD5Hash = async (teamspace, project, federation, user) => { const { subModels: containers } = await getFederationById(teamspace, federation, { subModels: 1 }); - const containerWithMetadata = await getContainers( - teamspace, - containers.map((container) => container._id), - { _id: 1, name: 1, permissions: 1 }); - - const listOfPromises = containerWithMetadata.map( - async (container) => { - const hasAccess = await hasReadAccessToContainer(teamspace, project, container._id, user); - if (hasAccess) { - return getModelMD5Hash(teamspace, container._id); - } - return undefined; - }, - ); - - const promiseResponses = await Promise.allSettled(listOfPromises); - const responses = promiseResponses.flatMap(({ status, value }) => (status === 'fulfilled' && value ? value : [])); - return responses; + if (containers) { + const containerWithMetadata = await getContainers( + teamspace, + containers.map((container) => container._id), + { _id: 1, name: 1, permissions: 1 }); + + const listOfPromises = containerWithMetadata.map( + async (container) => { + const hasAccess = await hasReadAccessToContainer(teamspace, project, container._id, user); + if (hasAccess) { + return getModelMD5Hash(teamspace, container._id); + } + return undefined; + }, + ); + + const promiseResponses = await Promise.allSettled(listOfPromises); + const responses = promiseResponses.flatMap(({ status, value }) => (status === 'fulfilled' && value ? value : [])); + + return responses; + } + return []; }; module.exports = Federations; diff --git a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js index 2ea3d937c03..d6566ffa536 100644 --- a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js +++ b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js @@ -61,6 +61,24 @@ const containers = [ }, ]; +const containersNoRev = [ + { + _id: ServiceHelper.generateUUIDString(), + name: ServiceHelper.generateRandomString(), + properties: { + ...ServiceHelper.generateRandomModelProperties(modelTypes.CONTAINER), + permissions: [{ user: users.viewer.user, permission: 'viewer' }, { user: users.commenter.user, permission: 'commenter' }], + }, + }, + { + _id: ServiceHelper.generateUUIDString(), + name: ServiceHelper.generateRandomString(), + properties: { + ...ServiceHelper.generateRandomModelProperties(modelTypes.CONTAINER), + }, + }, +]; + const models = [ { _id: ServiceHelper.generateUUIDString(), @@ -79,17 +97,28 @@ const models = [ ...ServiceHelper.generateRandomModelProperties(modelTypes.FEDERATION), permissions: [{ user: users.viewer.user, permission: 'viewer' }, { user: users.commenter.user, permission: 'commenter' }], federate: true, - subModels: containers.map((model) => ({ _id: model._id })), + subModels: containersNoRev.map((model) => ({ _id: model._id })), }, }, { _id: ServiceHelper.generateUUIDString(), name: ServiceHelper.generateRandomString(), - properties: ServiceHelper.generateRandomModelProperties(), + properties: { + ...ServiceHelper.generateRandomModelProperties(), + federate: true, + subModels: [], + }, + }, + { + _id: ServiceHelper.generateUUIDString(), + name: ServiceHelper.generateRandomString(), + properties: { + ...ServiceHelper.generateRandomModelProperties(), + }, }, ]; -const container = models[2]; +const container = models[3]; const anotherFed = models[1]; const conRevisions = ServiceHelper.generateRevisionEntry(); @@ -111,6 +140,12 @@ const setupData = async () => { subModel.name, subModel.properties, )); + const containerNoRevProms = containersNoRev.map((subModel) => ServiceHelper.db.createModel( + teamspace, + subModel._id, + subModel.name, + subModel.properties, + )); const revisionsProms = containers.map((subModel) => ServiceHelper.db.createRevision( teamspace, project.id, @@ -119,12 +154,19 @@ const setupData = async () => { modelTypes.CONTAINER, )); + const projectModels = [ + ...models.map(({ _id }) => _id), + ...containers.map(({ _id }) => _id), + ...containerNoRevProms.map(({ _id }) => _id), + ]; + return Promise.all([ ...userProms, ...modelProms, ...containerProms, + ...containerNoRevProms, ...revisionsProms, - ServiceHelper.db.createProject(teamspace, project.id, project.name, models.map(({ _id }) => _id)), + ServiceHelper.db.createProject(teamspace, project.id, project.name, projectModels), ServiceHelper.db.createUser(nobody), ]); }; @@ -248,13 +290,15 @@ const testGetFederationMD5Hash = () => { })); return [ - // ['there is no valid session key.', { ...parameters, key: null }, false, templates.notLoggedIn], - // ['the user is not a member of the teamspace.', { ...parameters, key: nobody.apiKey }, false, templates.teamspaceNotFound], - // ['the user does not have access to the project.', { ...parameters, key: users.noProjectAccess.apiKey }, false, templates.notAuthorized], - // ['the teamspace does not exist.', { ...parameters, ts: ServiceHelper.generateUUIDString() }, false, templates.teamspaceNotFound], - // ['the federation does not exist.', { ...parameters, modelId: ServiceHelper.generateUUIDString() }, false, templates.federationNotFound], + ['there is no valid session key.', { ...parameters, key: null }, false, templates.notLoggedIn], + ['the user is not a member of the teamspace.', { ...parameters, key: nobody.apiKey }, false, templates.teamspaceNotFound], + ['the user does not have access to the project.', { ...parameters, key: users.noProjectAccess.apiKey }, false, templates.notAuthorized], + ['the teamspace does not exist.', { ...parameters, ts: ServiceHelper.generateUUIDString() }, false, templates.teamspaceNotFound], + ['the federation does not exist.', { ...parameters, modelId: ServiceHelper.generateUUIDString() }, false, templates.federationNotFound], ['the viewer access it and return just that information.', { ...parameters, key: users.viewer.apiKey, response: viewerResponse }, true], ['the admin access it and return all the information.', { ...parameters, response: adminResponse }, true], + ['the admin access it but the federation is empty.', { ...parameters, modelId: models[2]._id }, true], + ['the admin access it but the containers in federation have no revisions.', { ...parameters, modelId: models[1]._id }, true], ]; }; @@ -287,6 +331,6 @@ describe(ServiceHelper.determineTestGroup(__filename), () => { ServiceHelper.closeApp(server), ])); - // testNewRevision(); + testNewRevision(); testGetFederationMD5Hash(); }); diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js index 71e057af68d..c51fae466b1 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js @@ -24,7 +24,6 @@ const { const fs = require('fs/promises'); const path = require('path'); -const UUIDParse = require('uuid-parse'); const CryptoJs = require('crypto-js'); jest.mock('../../../../../../../src/v5/utils/helper/models'); @@ -46,7 +45,6 @@ jest.mock('../../../../../../../src/v5/models/legends'); jest.mock('../../../../../../../src/v5/services/filesManager'); const FilesManager = require(`${src}/services/filesManager`); jest.mock('../../../../../../../src/v5/models/fileRefs'); -const FileRefs = require(`${src}/models/fileRefs`); jest.mock('../../../../../../../src/v5/handler/queue'); const QueueHandler = require(`${src}/handler/queue`); @@ -521,17 +519,16 @@ const testDownloadRevisionFiles = () => { const testGetMD5Hash = () => { describe('Get revision MD5 hash', () => { test('should return empty if revision has no file', async () => { - // given: Revisions.getRevisionByIdOrTag.mockResolvedValueOnce(templates.revisionNotFound); + const randomRevision = generateUUIDString(); - // it should - await expect(Containers.getRevisionMD5Hash('teamspace', 'container', generateUUIDString())).resolves.toEqual({}); + await expect(Containers.getRevisionMD5Hash('teamspace', 'container', randomRevision)).resolves.toEqual({}); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(1); - expect(FilesManager.getMD5FileHash).toHaveBeenCalledTimes(0); + expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledWith('teamspace', 'container', 'container', randomRevision, { fileSize: 1, rFile: 1, tag: 1, timestamp: 1 }, { includeVoid: false }); + expect(FilesManager.getMD5FileHash).not.toHaveBeenCalled(); }); test('should return an object if revision has a valid file and the file should be retrieved if no MD5Hash exists in the fileRef', async () => { - // given: const revisionMock = { _id: generateRandomString(), rFile: ['success!'], timestamp: new Date(), tag: 'testTag' }; const revisionCodeMock = generateUUIDString(); const fileHash = { hash: CryptoJs.MD5(revisionMock._id).toString(), size: 100 }; @@ -539,7 +536,6 @@ const testGetMD5Hash = () => { Revisions.getRevisionByIdOrTag.mockResolvedValueOnce(revisionMock); FilesManager.getMD5FileHash.mockResolvedValueOnce(fileHash); - // it should await expect(Containers.getRevisionMD5Hash('teamspace', 'container', revisionCodeMock)).resolves.toEqual({ container: 'container', code: revisionMock.tag, @@ -550,7 +546,9 @@ const testGetMD5Hash = () => { }); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(1); + expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledWith('teamspace', 'container', 'container', revisionCodeMock, { fileSize: 1, rFile: 1, tag: 1, timestamp: 1 }, { includeVoid: false }); expect(FilesManager.getMD5FileHash).toHaveBeenCalledTimes(1); + expect(FilesManager.getMD5FileHash).toHaveBeenCalledWith('teamspace', 'container', revisionMock.rFile[0]); }); }); }; diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js index 0c3aaec4456..7add7217c23 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js @@ -162,7 +162,7 @@ jest.mock('../../../../../../../src/v5/utils/permissions/permissions', () => ({ ...jest.requireActual('../../../../../../../src/v5/utils/permissions/permissions'), isTeamspaceAdmin: jest.fn().mockImplementation((teamspace, user) => user === 'tsAdmin'), hasProjectAdminPermissions: jest.fn().mockImplementation((perm, user) => user === 'projAdmin'), - hasReadAccessToContainer: jest.fn().mockImplementation((teamspace, project, modelID, username) => username === 'tsAdmin' + hasReadAccessToContainer: jest.fn().mockImplementation((teamspace, proj, modelID, username) => username === 'tsAdmin' || username === 'projAdmin' || mockContainers.filter((element) => element._id === modelID)[0].permissions.some((element) => element.user === username), ), })); @@ -383,7 +383,7 @@ const testGetMD5Hash = () => { return mockContainers.filter((container) => container._id === containers[0]); }); ModelSettings.getContainerById.mockImplementation( - (ts, container, project) => mockContainers.filter((cntr) => cntr._id === container)); + (ts, container, proj) => mockContainers.filter((cntr) => cntr._id === container)); ProjectSettings.modelsExistInProject.mockResolvedValue(true); test('should get an empty array if user does not have rights to the container', async () => { @@ -391,8 +391,9 @@ const testGetMD5Hash = () => { await expect(Federations.getMD5Hash('teamspace', 'federation', 'NoAcessUser')).resolves.toEqual([]); expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); - expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); - expect(Revisions.getLatestRevision).toHaveBeenCalledTimes(0); + expect(ModelSettings.getContainers).toHaveBeenCalledWith('teamspace', ['1', '2', '3'], { _id: 1, name: 1, permissions: 1 }); + expect(Revisions.getRevisionByIdOrTag).not.toHaveBeenCalled(); + expect(Revisions.getLatestRevision).not.toHaveBeenCalled(); }); test('it should return just the containers users have access to', async () => { @@ -407,10 +408,14 @@ const testGetMD5Hash = () => { filename: revisionMock.rFile[0], size: fileEntry.size }]); expect(ModelSettings.getFederationById).toHaveBeenCalledTimes(1); + expect(ModelSettings.getFederationById).toHaveBeenCalledWith('teamspace', 'federation', { subModels: 1 }); expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); - expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); + expect(ModelSettings.getContainers).toHaveBeenCalledWith('teamspace', ['1', '2', '3'], { _id: 1, name: 1, permissions: 1 }); + expect(Revisions.getRevisionByIdOrTag).not.toHaveBeenCalled(); expect(Revisions.getLatestRevision).toHaveBeenCalledTimes(1); + expect(Revisions.getLatestRevision).toHaveBeenCalledWith('teamspace', '1', 'container', { fileSize: 1, rFile: 1, tag: 1, timestamp: 1 }); expect(FilesManager.getMD5FileHash).toHaveBeenCalledTimes(1); + expect(FilesManager.getMD5FileHash).toHaveBeenCalledWith('teamspace', '1', 'success!'); }); test('it should return an array with all the containers if admin', async () => { @@ -443,9 +448,14 @@ const testGetMD5Hash = () => { size: fileEntry.size, }]); expect(ModelSettings.getFederationById).toHaveBeenCalledTimes(1); + expect(ModelSettings.getFederationById).toHaveBeenCalledWith('teamspace', 'federation', { subModels: 1 }); expect(ModelSettings.getContainers).toHaveBeenCalledTimes(1); - expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(0); + expect(ModelSettings.getContainers).toHaveBeenCalledWith('teamspace', ['1', '2', '3'], { _id: 1, name: 1, permissions: 1 }); + expect(Revisions.getRevisionByIdOrTag).not.toHaveBeenCalled(); expect(Revisions.getLatestRevision).toHaveBeenCalledTimes(3); + expect(Revisions.getLatestRevision).toHaveBeenNthCalledWith(1, 'teamspace', '1', 'container', { fileSize: 1, rFile: 1, tag: 1, timestamp: 1 }); + expect(Revisions.getLatestRevision).toHaveBeenNthCalledWith(2, 'teamspace', '2', 'container', { fileSize: 1, rFile: 1, tag: 1, timestamp: 1 }); + expect(Revisions.getLatestRevision).toHaveBeenNthCalledWith(3, 'teamspace', '3', 'container', { fileSize: 1, rFile: 1, tag: 1, timestamp: 1 }); }); }); }; From c2ef5e8cb9cf7eb1ab2f429f511b289bbb046a3b Mon Sep 17 00:00:00 2001 From: Bogdan Cornea Date: Wed, 15 Jan 2025 10:16:07 +0000 Subject: [PATCH 12/15] test coverage fix #ISSUE_5287 --- .../teamspaces/projects/models/federations.test.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js index 7add7217c23..1cc165eb5e0 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js @@ -383,7 +383,7 @@ const testGetMD5Hash = () => { return mockContainers.filter((container) => container._id === containers[0]); }); ModelSettings.getContainerById.mockImplementation( - (ts, container, proj) => mockContainers.filter((cntr) => cntr._id === container)); + (ts, container) => mockContainers.filter((cntr) => cntr._id === container)); ProjectSettings.modelsExistInProject.mockResolvedValue(true); test('should get an empty array if user does not have rights to the container', async () => { @@ -457,6 +457,18 @@ const testGetMD5Hash = () => { expect(Revisions.getLatestRevision).toHaveBeenNthCalledWith(2, 'teamspace', '2', 'container', { fileSize: 1, rFile: 1, tag: 1, timestamp: 1 }); expect(Revisions.getLatestRevision).toHaveBeenNthCalledWith(3, 'teamspace', '3', 'container', { fileSize: 1, rFile: 1, tag: 1, timestamp: 1 }); }); + + test('it should return an empty array if the federation has no containers added', async () => { + ModelSettings.getFederationById.mockResolvedValueOnce({ }); + + await expect(Federations.getMD5Hash('teamspace', 'project', 'federation', 'tsAdmin')).resolves.toEqual([]); + + expect(ModelSettings.getFederationById).toHaveBeenCalledTimes(1); + expect(ModelSettings.getFederationById).toHaveBeenCalledWith('teamspace', 'federation', { subModels: 1 }); + expect(ModelSettings.getContainers).not.toHaveBeenCalled(); + expect(Revisions.getRevisionByIdOrTag).not.toHaveBeenCalled(); + expect(Revisions.getLatestRevision).not.toHaveBeenCalled(); + }); }); }; From 9c95df64536c0c77d8b5d11de2f9bf9c72c1933f Mon Sep 17 00:00:00 2001 From: Bogdan Cornea Date: Fri, 17 Jan 2025 09:38:17 +0000 Subject: [PATCH 13/15] Update backend/src/v5/services/filesManager.js Co-authored-by: Carmen Fan --- backend/src/v5/services/filesManager.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/v5/services/filesManager.js b/backend/src/v5/services/filesManager.js index 79e0f12b068..e8d477a79a4 100644 --- a/backend/src/v5/services/filesManager.js +++ b/backend/src/v5/services/filesManager.js @@ -219,7 +219,11 @@ FilesManager.storeFileStream = async (teamspace, collection, id, dataStream, met FilesManager.getMD5FileHash = async (teamspace, container, filename) => { const refEntry = await getRefEntry(teamspace, `${container}.history.ref`, filename); - if (Object.keys(refEntry).includes('MD5Hash')) { +const {size, md5Hash: hash }= await getRefEntry(teamspace, `${container}.history.ref`, filename); + +if(hash) { + return {size, hash}; +} return { hash: refEntry.MD5Hash, size: refEntry.size, From 3e3f9c0a3ff3798c4b78e20028b5336364b27468 Mon Sep 17 00:00:00 2001 From: Bogdan Cornea Date: Mon, 20 Jan 2025 16:41:09 +0000 Subject: [PATCH 14/15] address latest comments #ISSUE_5287 --- .../projects/models/commons/revisions.js | 7 ++ .../projects/models/commons/modelList.js | 9 +- .../projects/models/common/revisions.js | 15 +-- .../projects/models/federations/revisions.js | 93 ++++++++++--------- backend/src/v5/services/filesManager.js | 27 ++---- .../projects/models/common/revisions.test.js | 4 +- .../models/federations/revisions.test.js | 21 +++-- .../projects/models/commons/revisions.test.js | 27 ++++++ .../projects/models/containers.test.js | 6 +- .../projects/models/federations.test.js | 18 ++-- .../v5/unit/services/filesManager.test.js | 5 +- 11 files changed, 133 insertions(+), 99 deletions(-) diff --git a/backend/src/v5/middleware/dataConverter/outputs/teamspaces/projects/models/commons/revisions.js b/backend/src/v5/middleware/dataConverter/outputs/teamspaces/projects/models/commons/revisions.js index a58b0f28e0f..21a80f11eeb 100644 --- a/backend/src/v5/middleware/dataConverter/outputs/teamspaces/projects/models/commons/revisions.js +++ b/backend/src/v5/middleware/dataConverter/outputs/teamspaces/projects/models/commons/revisions.js @@ -40,4 +40,11 @@ Revision.serialiseRevisionArray = (req, res) => { respond(req, res, templates.ok, { revisions }); }; +Revision.serialiseRevision = (req, res) => { + const rev = req.outputData; + const revision = serialiseRevision(rev); + + respond(req, res, templates.ok, revision); +}; + module.exports = Revision; diff --git a/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js b/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js index 313f55f64dd..6c8814ba485 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js +++ b/backend/src/v5/processors/teamspaces/projects/models/commons/modelList.js @@ -78,15 +78,14 @@ ModelList.getModelMD5Hash = async (teamspace, container, revision) => { if (!rev.rFile?.length) return {}; - const code = rev.tag; - const uploadedAt = new Date(rev.timestamp).getTime(); + const { tag, timestamp } = rev; const filename = rev.rFile[0]; - const { hash, size } = await getMD5FileHash(teamspace, container, filename); + const { hash, size } = await getMD5FileHash(teamspace, `${container}.history`, filename); return { container, - code, - uploadedAt, + tag, + timestamp, hash, filename, size, diff --git a/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js b/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js index 3db87d70db2..0f545fcca06 100644 --- a/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js +++ b/backend/src/v5/routes/teamspaces/projects/models/common/revisions.js @@ -17,12 +17,12 @@ const { hasReadAccessToContainer, hasReadAccessToDrawing, hasWriteAccessToContainer, hasWriteAccessToDrawing } = require('../../../../../middleware/permissions/permissions'); const { respond, writeStreamRespond } = require('../../../../../utils/responder'); +const { serialiseRevision, serialiseRevisionArray } = require('../../../../../middleware/dataConverter/outputs/teamspaces/projects/models/commons/revisions'); const Containers = require('../../../../../processors/teamspaces/projects/models/containers'); const Drawings = require('../../../../../processors/teamspaces/projects/models/drawings'); const { Router } = require('express'); const { getUserFromSession } = require('../../../../../utils/sessions'); const { modelTypes } = require('../../../../../models/modelSettings.constants'); -const { serialiseRevisionArray } = require('../../../../../middleware/dataConverter/outputs/teamspaces/projects/models/commons/revisions'); const { templates } = require('../../../../../utils/responseCodes'); const { validateNewRevisionData: validateNewContainerRev } = require('../../../../../middleware/dataConverter/inputs/teamspaces/projects/models/containers'); const { validateNewRevisionData: validateNewDrawingRev } = require('../../../../../middleware/dataConverter/inputs/teamspaces/projects/models/drawings'); @@ -59,12 +59,13 @@ const getRevisions = (modelType) => async (req, res, next) => { } }; -const getRevisionMD5Hash = () => async (req, res) => { +const getRevisionMD5Hash = () => async (req, res, next) => { const { teamspace, container, revision } = req.params; try { const response = await Containers.getRevisionMD5Hash(teamspace, container, revision); - respond(req, res, templates.ok, response); + req.outputData = response; + next(); } catch (err) { /* istanbul ignore next */ respond(req, res, err); @@ -498,11 +499,11 @@ const establishRoutes = (modelType) => { * type: string * description: Container ID * example: ef0855b6-4cc7-4be1-b2d6-c032dce7806a - * code: + * tag: * type: string - * description: Revision Code + * description: Revision Tag * example: X01 - * uploadedAt: + * timestamp: * type: number * description: Upload date * example: 1435068682 @@ -519,7 +520,7 @@ const establishRoutes = (modelType) => { * description: File size in bytes * example: 329487234 */ - router.get('/:revision/files/original/info', hasReadAccessToContainer, getRevisionMD5Hash()); + router.get('/:revision/files/original/info', hasReadAccessToContainer, getRevisionMD5Hash(), serialiseRevision); } if (modelType === modelTypes.DRAWING) { /** diff --git a/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js b/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js index 924f0465727..52e54d79adb 100644 --- a/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js +++ b/backend/src/v5/routes/teamspaces/projects/models/federations/revisions.js @@ -20,6 +20,7 @@ const Federations = require('../../../../../processors/teamspaces/projects/model const { Router } = require('express'); const { getUserFromSession } = require('../../../../../utils/sessions'); const { respond } = require('../../../../../utils/responder'); +const { serialiseRevisionArray } = require('../../../../../middleware/dataConverter/outputs/teamspaces/projects/models/commons/revisions'); const { templates } = require('../../../../../utils/responseCodes'); const { validateNewRevisionData } = require('../../../../../middleware/dataConverter/inputs/teamspaces/projects/models/federations'); @@ -36,13 +37,13 @@ const createNewFederationRevision = async (req, res) => { } }; -const getFederationMD5Hash = async (req, res) => { +const getFederationMD5Hash = async (req, res, next) => { const { teamspace, project, federation } = req.params; const user = getUserFromSession(req.session); try { - const response = await Federations.getMD5Hash(teamspace, project, federation, user); - respond(req, res, templates.ok, response); + req.outputData = await Federations.getMD5Hash(teamspace, project, federation, user); + next(); } catch (err) { /* istanbul ignore next */ respond(req, res, err); @@ -155,49 +156,53 @@ const establishRoutes = () => { * content: * application/json: * schema: - * type: array - * items: - * - type: object - * properties: - * container: - * type: string - * description: Container ID - * example: ef0855b6-4cc7-4be1-b2d6-c032dce7806a - * code: - * type: string - * description: Revision Code - * example: X01 - * uploadedAt: - * type: number - * description: Upload date - * example: 1435068682 - * hash: - * type: string - * description: MD5 hash of the original file uploaded - * example: 76dea970d89477ed03dc5289f297443c - * filename: - * type: string - * description: Name of the file - * example: test.rvt - * size: - * type: number - * description: File size in bytes - * example: 329487234 + * type: object + * properties: + * revisions: + * type: array + * items: + * - type: object + * properties: + * container: + * type: string + * description: Container ID + * example: ef0855b6-4cc7-4be1-b2d6-c032dce7806a + * tag: + * type: string + * description: Container tag + * example: X01 + * timestamp: + * type: number + * description: Upload date + * example: 1435068682 + * hash: + * type: string + * description: MD5 hash of the original file uploaded + * example: 76dea970d89477ed03dc5289f297443c + * filename: + * type: string + * description: Name of the file + * example: test.rvt + * size: + * type: number + * description: File size in bytes + * example: 329487234 * example: - * - container: ef0855b6-4cc7-4be1-b2d6-c032dce7806a - * code: X01 - * uploadedAt: 1711929600 - * hash: 14703cffd1a95017a95eb092315c3ee1 - * filename: test_1.rvt - * size: 123456789 - * - container: ef0855b6-4bb7-4be1-b2d6-c032dce7806a - * code: X01 - * uploadedAt: 1711929690 - * hash: 14703cffd1a95017a95eb092315c3ee1 - * filename: test_2.rvt - * size: 123456780 + * revisions: + * - container: ef0855b6-4cc7-4be1-b2d6-c032dce7806a + * code: X01 + * uploadedAt: 1711929600 + * hash: 14703cffd1a95017a95eb092315c3ee1 + * filename: test_1.rvt + * size: 123456789 + * - container: ef0855b6-4bb7-4be1-b2d6-c032dce7806a + * code: X01 + * uploadedAt: 1711929690 + * hash: 14703cffd1a95017a95eb092315c3ee1 + * filename: test_2.rvt + * size: 123456780 */ - router.get('/:revision/files/original/info', hasReadAccessToFederation, getFederationMD5Hash); + router.get('/:revision/files/original/info', hasReadAccessToFederation, getFederationMD5Hash, serialiseRevisionArray); return router; }; diff --git a/backend/src/v5/services/filesManager.js b/backend/src/v5/services/filesManager.js index e8d477a79a4..86bc824e13d 100644 --- a/backend/src/v5/services/filesManager.js +++ b/backend/src/v5/services/filesManager.js @@ -216,28 +216,21 @@ FilesManager.storeFileStream = async (teamspace, collection, id, dataStream, met await insertRef(teamspace, collection, { ...meta, ...refInfo, _id: id, mimeType }); }; -FilesManager.getMD5FileHash = async (teamspace, container, filename) => { - const refEntry = await getRefEntry(teamspace, `${container}.history.ref`, filename); - -const {size, md5Hash: hash }= await getRefEntry(teamspace, `${container}.history.ref`, filename); - -if(hash) { - return {size, hash}; -} - return { - hash: refEntry.MD5Hash, - size: refEntry.size, - }; +FilesManager.getMD5FileHash = async (teamspace, collection, filename) => { + const { size, MD5Hash: hash } = await getRefEntry(teamspace, collection, filename); + + if (hash) { + return { size, hash }; } - const file = await FilesManager.getFile(teamspace, `${container}.history`, filename); - const hash = CryptoJs.MD5(file).toString(); + const file = await FilesManager.getFile(teamspace, collection, filename); + const newHash = CryptoJs.MD5(file).toString(); - await updateRef(teamspace, `${container}.history.ref`, { _id: filename }, { $set: { MD5Hash: hash } }); + await updateRef(teamspace, collection, { _id: filename }, { $set: { MD5Hash: hash } }); return { - hash, - size: refEntry.size, + hash: newHash, + size, }; }; diff --git a/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js b/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js index 7f7e77edd25..c7dfe5d3026 100644 --- a/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js +++ b/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/revisions.test.js @@ -515,8 +515,8 @@ const testGetRevisionMD5Hash = () => { const MD5HashResponseExpectation = { container: model._id, - code: revision.tag, - uploadedAt: new Date(revision.timestamp).getTime(), + tag: revision.tag, + timestamp: new Date(revision.timestamp).getTime(), hash: CryptoJs.MD5(Buffer.from(revision.rFile[0])).toString(), filename: revision.rFile[0], size: 20, diff --git a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js index d6566ffa536..4e5810cef70 100644 --- a/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js +++ b/backend/tests/v5/e2e/routes/teamspaces/projects/models/federations/revisions.test.js @@ -270,24 +270,24 @@ const testGetFederationMD5Hash = () => { modelId: models[0]._id, revisionId: ServiceHelper.generateUUIDString(), key: users.tsAdmin.apiKey, - response: [], + response: { revisions: [] }, }; - const viewerResponse = [{ + const viewerResponse = { revisions: [{ container: containers[0]._id, - code: conRevisions.tag, - uploadedAt: new Date(conRevisions.timestamp).getTime(), + tag: conRevisions.tag, + timestamp: new Date(conRevisions.timestamp).getTime(), hash: CryptoJs.MD5(Buffer.from(conRevisions.rFile[0])).toString(), filename: conRevisions.rFile[0], size: 20, - }]; - const adminResponse = containers.map((model) => ({ + }] }; + const adminResponse = { revisions: containers.map((model) => ({ container: model._id, - code: conRevisions.tag, - uploadedAt: new Date(conRevisions.timestamp).getTime(), + tag: conRevisions.tag, + timestamp: new Date(conRevisions.timestamp).getTime(), hash: CryptoJs.MD5(Buffer.from(conRevisions.rFile[0])).toString(), filename: conRevisions.rFile[0], size: 20, - })); + })) }; return [ ['there is no valid session key.', { ...parameters, key: null }, false, templates.notLoggedIn], @@ -310,7 +310,8 @@ const testGetFederationMD5Hash = () => { const res = await agent.get(`${route(parameters)}`).expect(expectedStatus); if (success) { - outOfOrderArrayEqual(JSON.parse(res.text), parameters.response); + const result = JSON.parse(res.text); + outOfOrderArrayEqual(result.revisions, parameters.response.revisions); } else { expect(res.body.code).toEqual(error.code); } diff --git a/backend/tests/v5/unit/middleware/dataConverter/outputs/teamspaces/projects/models/commons/revisions.test.js b/backend/tests/v5/unit/middleware/dataConverter/outputs/teamspaces/projects/models/commons/revisions.test.js index 0e569659ebc..18726d47268 100644 --- a/backend/tests/v5/unit/middleware/dataConverter/outputs/teamspaces/projects/models/commons/revisions.test.js +++ b/backend/tests/v5/unit/middleware/dataConverter/outputs/teamspaces/projects/models/commons/revisions.test.js @@ -64,6 +64,33 @@ const testSerialiseRevisionArray = () => { }); }; +const testSerialiseRevision = () => { + const noTs = generateRevisionEntry(); + delete noTs.timestamp; + describe.each([ + [{}, 'no revision'], + [noTs, 'no timestamp revision'], + [generateRevisionEntry(), 'regual revision'], + ])('Serialize revision data', (data, desc) => { + test(`should serialise correctly with ${desc}`, () => { + const nextIdx = respondFn.mock.calls.length; + RevisionOutputMiddlewares.serialiseRevision({ outputData: cloneDeep(data) }, {}, () => {}); + + expect(respondFn.mock.calls.length).toBe(nextIdx + 1); + expect(respondFn.mock.calls[nextIdx][2]).toEqual(templates.ok); + + const serialisedRev = { + ...data, + _id: UUIDToString(data._id), + timestamp: data.timestamp ? data.timestamp.getTime() : data.timestamp, + }; + + expect(respondFn.mock.calls[nextIdx][3]).toEqual(serialisedRev); + }); + }); +}; + describe('middleware/dataConverter/outputs/teamspaces/projects/models/commons/revisions', () => { testSerialiseRevisionArray(); + testSerialiseRevision(); }); diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js index c51fae466b1..97109a9ac83 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/containers.test.js @@ -538,8 +538,8 @@ const testGetMD5Hash = () => { await expect(Containers.getRevisionMD5Hash('teamspace', 'container', revisionCodeMock)).resolves.toEqual({ container: 'container', - code: revisionMock.tag, - uploadedAt: new Date(revisionMock.timestamp).getTime(), + tag: revisionMock.tag, + timestamp: revisionMock.timestamp, hash: fileHash.hash, filename: revisionMock.rFile[0], size: fileHash.size, @@ -548,7 +548,7 @@ const testGetMD5Hash = () => { expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledTimes(1); expect(Revisions.getRevisionByIdOrTag).toHaveBeenCalledWith('teamspace', 'container', 'container', revisionCodeMock, { fileSize: 1, rFile: 1, tag: 1, timestamp: 1 }, { includeVoid: false }); expect(FilesManager.getMD5FileHash).toHaveBeenCalledTimes(1); - expect(FilesManager.getMD5FileHash).toHaveBeenCalledWith('teamspace', 'container', revisionMock.rFile[0]); + expect(FilesManager.getMD5FileHash).toHaveBeenCalledWith('teamspace', 'container.history', revisionMock.rFile[0]); }); }); }; diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js index 1cc165eb5e0..8a2a095d2cd 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/federations.test.js @@ -402,8 +402,8 @@ const testGetMD5Hash = () => { FilesManager.getMD5FileHash.mockResolvedValueOnce(mockMD5Hash); await expect(Federations.getMD5Hash('teamspace', 'project', 'federation', 'user1')).resolves.toEqual([{ container: '1', - code: revisionMock.tag, - uploadedAt: new Date(revisionMock.timestamp).getTime(), + tag: revisionMock.tag, + timestamp: revisionMock.timestamp, hash: CryptoJs.MD5(revisionMock._id).toString(), filename: revisionMock.rFile[0], size: fileEntry.size }]); @@ -415,7 +415,7 @@ const testGetMD5Hash = () => { expect(Revisions.getLatestRevision).toHaveBeenCalledTimes(1); expect(Revisions.getLatestRevision).toHaveBeenCalledWith('teamspace', '1', 'container', { fileSize: 1, rFile: 1, tag: 1, timestamp: 1 }); expect(FilesManager.getMD5FileHash).toHaveBeenCalledTimes(1); - expect(FilesManager.getMD5FileHash).toHaveBeenCalledWith('teamspace', '1', 'success!'); + expect(FilesManager.getMD5FileHash).toHaveBeenCalledWith('teamspace', '1.history', 'success!'); }); test('it should return an array with all the containers if admin', async () => { @@ -427,22 +427,22 @@ const testGetMD5Hash = () => { outOfOrderArrayEqual(await Federations.getMD5Hash('teamspace', 'project', 'federation', 'tsAdmin'), [ { container: '1', - code: revisionMock.tag, - uploadedAt: new Date(revisionMock.timestamp).getTime(), + tag: revisionMock.tag, + timestamp: revisionMock.timestamp, hash: CryptoJs.MD5(revisionMock._id).toString(), filename: revisionMock.rFile[0], size: fileEntry.size, }, { container: '2', - code: revisionMock.tag, - uploadedAt: new Date(revisionMock.timestamp).getTime(), + tag: revisionMock.tag, + timestamp: revisionMock.timestamp, hash: CryptoJs.MD5(revisionMock._id).toString(), filename: revisionMock.rFile[0], size: fileEntry.size, }, { container: '3', - code: revisionMock.tag, - uploadedAt: new Date(revisionMock.timestamp).getTime(), + tag: revisionMock.tag, + timestamp: revisionMock.timestamp, hash: CryptoJs.MD5(revisionMock._id).toString(), filename: revisionMock.rFile[0], size: fileEntry.size, diff --git a/backend/tests/v5/unit/services/filesManager.test.js b/backend/tests/v5/unit/services/filesManager.test.js index 2af2af5d74f..803e246caa0 100644 --- a/backend/tests/v5/unit/services/filesManager.test.js +++ b/backend/tests/v5/unit/services/filesManager.test.js @@ -711,9 +711,10 @@ const testGetMD5FileHash = () => { expect(FileRefs.updateRef).toHaveBeenCalledTimes(1); }); test('it should return straight away if the MD5 has has been cached', async () => { - FileRefs.getRefEntry.mockResolvedValueOnce({ ...mockRefEntry, MD5Hash: CryptoJS.MD5(mockId).toString() }); + const mockRef = { ...mockRefEntry, MD5Hash: generateRandomString() }; + FileRefs.getRefEntry.mockResolvedValueOnce(mockRef); - await expect(FilesManager.getMD5FileHash('teamspace', 'container', 'filename')).resolves.toEqual({ hash: CryptoJS.MD5(mockId).toString(), size: 100 }); + await expect(FilesManager.getMD5FileHash('teamspace', 'container', 'filename')).resolves.toEqual({ hash: mockRef.MD5Hash, size: mockRef.size }); expect(FileRefs.getRefEntry).toHaveBeenCalledTimes(1); expect(FSHandler.getFile).toHaveBeenCalledTimes(0); From 53480b559e71e215d866b9a0f5cc90115a41caf9 Mon Sep 17 00:00:00 2001 From: Bogdan Cornea Date: Tue, 21 Jan 2025 09:47:39 +0000 Subject: [PATCH 15/15] es6 naming convention - change MD5Hash to md5Hash #ISSUE_5287 --- backend/src/v5/services/filesManager.js | 4 ++-- backend/tests/v5/unit/services/filesManager.test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/v5/services/filesManager.js b/backend/src/v5/services/filesManager.js index 86bc824e13d..fec69a6cd63 100644 --- a/backend/src/v5/services/filesManager.js +++ b/backend/src/v5/services/filesManager.js @@ -217,7 +217,7 @@ FilesManager.storeFileStream = async (teamspace, collection, id, dataStream, met }; FilesManager.getMD5FileHash = async (teamspace, collection, filename) => { - const { size, MD5Hash: hash } = await getRefEntry(teamspace, collection, filename); + const { size, md5Hash: hash } = await getRefEntry(teamspace, collection, filename); if (hash) { return { size, hash }; @@ -226,7 +226,7 @@ FilesManager.getMD5FileHash = async (teamspace, collection, filename) => { const file = await FilesManager.getFile(teamspace, collection, filename); const newHash = CryptoJs.MD5(file).toString(); - await updateRef(teamspace, collection, { _id: filename }, { $set: { MD5Hash: hash } }); + await updateRef(teamspace, collection, { _id: filename }, { $set: { md5Hash: hash } }); return { hash: newHash, diff --git a/backend/tests/v5/unit/services/filesManager.test.js b/backend/tests/v5/unit/services/filesManager.test.js index 803e246caa0..33d5f5aef2d 100644 --- a/backend/tests/v5/unit/services/filesManager.test.js +++ b/backend/tests/v5/unit/services/filesManager.test.js @@ -711,10 +711,10 @@ const testGetMD5FileHash = () => { expect(FileRefs.updateRef).toHaveBeenCalledTimes(1); }); test('it should return straight away if the MD5 has has been cached', async () => { - const mockRef = { ...mockRefEntry, MD5Hash: generateRandomString() }; + const mockRef = { ...mockRefEntry, md5Hash: generateRandomString() }; FileRefs.getRefEntry.mockResolvedValueOnce(mockRef); - await expect(FilesManager.getMD5FileHash('teamspace', 'container', 'filename')).resolves.toEqual({ hash: mockRef.MD5Hash, size: mockRef.size }); + await expect(FilesManager.getMD5FileHash('teamspace', 'container', 'filename')).resolves.toEqual({ hash: mockRef.md5Hash, size: mockRef.size }); expect(FileRefs.getRefEntry).toHaveBeenCalledTimes(1); expect(FSHandler.getFile).toHaveBeenCalledTimes(0);