From 16e9ef8684d20205663a5f279e3c940c0059e908 Mon Sep 17 00:00:00 2001 From: YashmeetSAP Date: Mon, 20 May 2024 19:46:44 +0530 Subject: [PATCH 1/4] Capability to attach same document to different entities --- index.cds | 4 ++ lib/handler/index.js | 99 +++++++++++++++++++++++++---- lib/persistence/index.js | 18 +++++- lib/sdm.js | 128 ++++++++++++++++++++++++++++++-------- lib/util/messageConsts.js | 2 + package.json | 3 +- 6 files changed, 214 insertions(+), 40 deletions(-) create mode 100644 index.cds diff --git a/index.cds b/index.cds new file mode 100644 index 0000000..7b1858e --- /dev/null +++ b/index.cds @@ -0,0 +1,4 @@ +using { Attachments} from '@cap-js/attachments'; +extend aspect Attachments with { + folderId : String @title: 'Folder ID'; +}; \ No newline at end of file diff --git a/lib/handler/index.js b/lib/handler/index.js index c803499..6eca2d5 100644 --- a/lib/handler/index.js +++ b/lib/handler/index.js @@ -4,27 +4,33 @@ const FormData = require("form-data"); async function readAttachment(Key, token, credentials) { try { - const document = await readDocument(Key, token, credentials.uri) - return document + const document = await readDocument(Key, token, credentials.uri); + return document; } catch (error) { throw new Error(error); } } -async function readDocument(Key, token, uri){ +async function readDocument(Key, token, uri) { const { repositoryId } = getConfigurations(); - const documentReadURL = uri+ "browser/" + repositoryId + "/root?objectID=" + Key + "&cmisselector=content"; + const documentReadURL = + uri + + "browser/" + + repositoryId + + "/root?objectID=" + + Key + + "&cmisselector=content"; const config = { - headers: {Authorization: `Bearer ${token}`}, - responseType: 'arraybuffer' + headers: { Authorization: `Bearer ${token}` }, + responseType: "arraybuffer", }; try { const response = await axios.get(documentReadURL, config); - const responseBuffer = Buffer.from(response.data, 'binary'); + const responseBuffer = Buffer.from(response.data, "binary"); return responseBuffer; } catch (error) { - let statusText = "An Error Occurred"; + let statusText = "An Error Occurred"; if (error.response && error.response.statusText) { statusText = error.response.statusText; } @@ -33,12 +39,65 @@ async function readDocument(Key, token, uri){ } } -async function createAttachment(data, credentials, token, attachments) { +async function getChildrenForFolderId(parentId, token, credentials) { + const { repositoryId } = getConfigurations(); + const getChildrenURL = + credentials.uri + + "browser/" + + repositoryId + + "/root?objectID=" + + parentId + + "&cmisselector=children"; + const config = { + headers: { Authorization: `Bearer ${token}` }, + }; + try { + const response = await axios.get(getChildrenURL, config); + return response.data.objects; + } catch (error) { + let statusText = "An Error Occurred"; + if (error.response && error.response.statusText) { + statusText = error.response.statusText; + } + throw new Error(statusText); + } +} + +async function createFolder(req, credentials, token, attachments) { + const up_ = attachments.keys.up_.keys[0].$generatedFieldName; + const idValue = up_.split("__")[1]; + const { repositoryId } = getConfigurations(); + const folderCreateURL = credentials.uri + "browser/" + repositoryId + "/root"; + const formData = new FormData(); + formData.append("cmisaction", "createFolder"); + formData.append("propertyId[0]", "cmis:name"); + formData.append("propertyValue[0]", req.data[idValue]); + formData.append("propertyId[1]", "cmis:objectTypeId"); + formData.append("propertyValue[1]", "cmis:folder"); + formData.append("succinct", "true"); + + let headers = formData.getHeaders(); + headers["Authorization"] = "Bearer " + token; + const config = { + headers: headers, + }; + return await updateServerRequest(folderCreateURL, formData, config); + //return response; +} + +async function createAttachment( + data, + credentials, + token, + attachments, + parentId +) { const { repositoryId } = getConfigurations(); const documentCreateURL = credentials.uri + "browser/" + repositoryId + "/root"; const formData = new FormData(); formData.append("cmisaction", "createDocument"); + formData.append("objectId", parentId); formData.append("propertyId[0]", "cmis:name"); formData.append("propertyValue[0]", data.filename); formData.append("propertyId[1]", "cmis:objectTypeId"); @@ -64,7 +123,7 @@ async function createAttachment(data, credentials, token, attachments) { return response; } -async function deleteAttachment(credentials, token, objectId, attachments) { +async function deleteAttachmentOrFolder(credentials, token, objectId) { const { repositoryId } = getConfigurations(); const documentDeleteURL = credentials.uri + "browser/" + repositoryId + "/root"; @@ -84,6 +143,21 @@ async function deleteAttachment(credentials, token, objectId, attachments) { return response; } +async function deleteFolderWithAttachments(credentials, token, parentId) { + const { repositoryId } = getConfigurations(); + const folderDeleteURL = credentials.uri + "browser/" + repositoryId + "/root"; + const formData = new FormData(); + formData.append("cmisaction", "deleteTree"); + formData.append("objectId", parentId); + let headers = formData.getHeaders(); + headers["Authorization"] = "Bearer " + token; + const config = { + headers: headers, + }; + const response = await updateServerRequest(folderDeleteURL, formData, config); + return response; +} + const updateServerRequest = async ( url, formData, @@ -101,7 +175,10 @@ const updateServerRequest = async ( }; module.exports = { + getChildrenForFolderId, + createFolder, createAttachment, - deleteAttachment, + deleteAttachmentOrFolder, + deleteFolderWithAttachments, readAttachment, }; diff --git a/lib/persistence/index.js b/lib/persistence/index.js index f72d95f..53dd4d7 100644 --- a/lib/persistence/index.js +++ b/lib/persistence/index.js @@ -14,10 +14,21 @@ async function getDraftAttachments(attachments, req) { .and({ HasActiveEntity: { "<>": 1 } }); } -async function getDuplicateAttachments(fileNames, attachments) { +async function getFolderIdForEntity(attachments, req) { + const up_ = attachments.keys.up_.keys[0].$generatedFieldName; + const idValue = up_.split("__")[1]; + return await SELECT.from(attachments) + .columns("folderId") + .where({ [up_]: req.data[idValue] }); +} + +async function getDuplicateAttachments(fileNames, attachments, req) { + const up_ = attachments.keys.up_.keys[0].$generatedFieldName; + const idValue = up_.split("__")[1]; return await SELECT.distinct(["filename"]) .from(attachments) - .where({ filename: { in: fileNames } }); + .where({ [up_]: req.data[idValue] }) + .and({ filename: { in: fileNames } }); } async function getURLsToDeleteFromAttachments(deletedAttachments, attachments) { @@ -29,5 +40,6 @@ module.exports = { getDraftAttachments, getDuplicateAttachments, getURLsToDeleteFromAttachments, - getURLFromAttachments + getURLFromAttachments, + getFolderIdForEntity, }; diff --git a/lib/sdm.js b/lib/sdm.js index 3825a36..0abe474 100644 --- a/lib/sdm.js +++ b/lib/sdm.js @@ -1,13 +1,24 @@ const cds = require("@sap/cds/lib"); -const { createAttachment, deleteAttachment, readAttachment } = require("../lib/handler"); +const { + getChildrenForFolderId, + createFolder, + createAttachment, + deleteAttachmentOrFolder, + deleteFolderWithAttachments, + readAttachment, +} = require("../lib/handler"); const { fetchAccessToken } = require("./util/index"); const { getDraftAttachments, getDuplicateAttachments, getURLsToDeleteFromAttachments, - getURLFromAttachments + getURLFromAttachments, + getFolderIdForEntity, } = require("../lib/persistence"); -const { duplicateFileErr } = require("./util/messageConsts"); +const { + duplicateFileErr, + duplicateDraftFileErr, +} = require("./util/messageConsts"); module.exports = class SDMAttachmentsService extends ( require("@cap-js/attachments/lib/basic") @@ -24,7 +35,7 @@ module.exports = class SDMAttachmentsService extends ( const response = await getURLFromAttachments(keys, attachments); const token = await fetchAccessToken(this.creds); try { - const Key = response?.url + const Key = response?.url; const content = await readAttachment(Key, token, this.creds); return content; } catch (error) { @@ -37,21 +48,43 @@ module.exports = class SDMAttachmentsService extends ( cds.model.definitions[req.query.target.name + ".attachments"]; const attachment_val = await getDraftAttachments(attachments, req); if (attachment_val.length > 0) { + //verify if duplicate files are added in drafts + const duplicateDraftFilesErrMsg = + this.isFileNameDuplicateInDrafts(attachment_val); + if (duplicateDraftFilesErrMsg != "") { + req.reject(409, duplicateDraftFileErr(duplicateDraftFilesErrMsg)); + } //verify if duplicates exist for the drafts const duplicateFilesErrMsg = await this.isFileNameDuplicate( attachment_val, - attachments + attachments, + req ); if (duplicateFilesErrMsg != "") { req.reject(409, duplicateFileErr(duplicateFilesErrMsg)); } const token = await fetchAccessToken(this.creds); + console.log("Token: ", token); + const folderIds = await getFolderIdForEntity(attachments, req); + let parentId = ""; + if (folderIds.length == 0) { + const response = await createFolder( + req, + this.creds, + token, + attachments + ); + parentId = response.data.succinctProperties["cmis:objectId"]; + } else { + parentId = folderIds[0].folderId; + } const failedReq = await this.onCreate( attachment_val, this.creds, token, attachments, - req + req, + parentId ); let errorResponse = ""; failedReq.forEach((attachment) => { @@ -62,14 +95,30 @@ module.exports = class SDMAttachmentsService extends ( } } - async isFileNameDuplicate(attachment_val, attachments) { + isFileNameDuplicateInDrafts(data) { + let fileNames = []; + for (let index in data) { + fileNames.push(data[index].filename); + } + let duplicates = [ + ...new Set( + fileNames.filter((value, index, self) => { + return self.indexOf(value) !== index; + }) + ), + ]; + return duplicates.join(", "); + } + + async isFileNameDuplicate(attachment_val, attachments, req) { let fileNames = []; for (let index in attachment_val) { fileNames.push(attachment_val[index].filename); } const are_duplicates = await getDuplicateAttachments( fileNames, - attachments + attachments, + req ); let duplicateFilesErrMsg = ""; are_duplicates.forEach((file) => { @@ -103,6 +152,13 @@ module.exports = class SDMAttachmentsService extends ( if (attachmentsToDelete.length > 0) { req.attachmentsToDelete = attachmentsToDelete; } + if (req.event == "DELETE") { + const folderIds = await getFolderIdForEntity(attachments, req); + if (folderIds.length > 0) { + const parentId = folderIds[0].folderId; + req.parentId = parentId; + } + } } } } @@ -115,12 +171,14 @@ module.exports = class SDMAttachmentsService extends ( if (req?.attachmentsToDelete?.length > 0) { const token = await fetchAccessToken(this.creds); + if (req?.parentId) { + await deleteFolderWithAttachments(this.creds, token, req.parentId); + } const deletePromises = req.attachmentsToDelete.map(async (attachment) => { - const deleteAttachmentResponse = await deleteAttachment( + const deleteAttachmentResponse = await deleteAttachmentOrFolder( this.creds, token, - attachment.url, - attachments + attachment.url ); const delData = await this.handleRequest( deleteAttachmentResponse, @@ -145,7 +203,7 @@ module.exports = class SDMAttachmentsService extends ( } } - async onCreate(data, credentials, token, attachments, req) { + async onCreate(data, credentials, token, attachments, req, parentId) { let failedReq = [], Ids = [], success = [], @@ -153,23 +211,43 @@ module.exports = class SDMAttachmentsService extends ( var i = 0; await Promise.all( data.map(async (d) => { - const response = await createAttachment( - d, - credentials, - token, - attachments - ); - if (response.status == 201) { - d.url = response.data.succinctProperties["cmis:objectId"]; - d.content = null; - success_ids.push(d.ID); - success.push(d); - } else { + // Check if d.content is null + if (d.content === null) { + failedReq.push( + `Content of file ${d.filename} is empty. Either it is corrupted or not uploaded properly.` + ); Ids.push(d.ID); - failedReq.push(response.response.data.message); + } else { + const response = await createAttachment( + d, + credentials, + token, + attachments, + parentId + ); + if (response.status == 201) { + d.folderId = parentId; + d.url = response.data.succinctProperties["cmis:objectId"]; + d.content = null; + success_ids.push(d.ID); + success.push(d); + } else { + Ids.push(d.ID); + failedReq.push(response.response.data.message); + } } }) ); + if (Ids.length > 0) { + const attachmentsInFolder = await getChildrenForFolderId( + parentId, + token, + credentials + ); + if (attachmentsInFolder.length == 0) { + await deleteAttachmentOrFolder(credentials, token, parentId); + } + } let removeCondition = (obj) => Ids.includes(obj.ID); req.data.attachments = req.data.attachments.filter( diff --git a/lib/util/messageConsts.js b/lib/util/messageConsts.js index aed4ce0..f26b9a4 100644 --- a/lib/util/messageConsts.js +++ b/lib/util/messageConsts.js @@ -1,2 +1,4 @@ module.exports.duplicateFileErr = (duplicateFiles) => `The files ${duplicateFiles} are already present in the repository. Please remove them from drafts, rename and try again.`; +module.exports.duplicateDraftFileErr = (duplicateDraftFiles) => + `The files ${duplicateDraftFiles} are added multiple times. Please remove them from drafts, rename and try again.`; diff --git a/package.json b/package.json index 5f01c7e..0521b7e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "cds-plugin.js", "files": [ "lib", - "srv" + "srv", + "index.cds" ], "keywords": [ "sap", From faed3250986c9ff9937ec567648334b866f3aeae Mon Sep 17 00:00:00 2001 From: YashmeetSAP Date: Tue, 21 May 2024 21:25:35 +0530 Subject: [PATCH 2/4] Fixes --- lib/handler/index.js | 25 +++++------- lib/sdm.js | 84 ++++++++++++++++++++------------------- lib/util/messageConsts.js | 2 + 3 files changed, 56 insertions(+), 55 deletions(-) diff --git a/lib/handler/index.js b/lib/handler/index.js index 6eca2d5..a3b779a 100644 --- a/lib/handler/index.js +++ b/lib/handler/index.js @@ -39,27 +39,25 @@ async function readDocument(Key, token, uri) { } } -async function getChildrenForFolderId(parentId, token, credentials) { +async function getFolderIdByPath(req, credentials, token, attachments) { + const up_ = attachments.keys.up_.keys[0].$generatedFieldName; + const idValue = up_.split("__")[1]; const { repositoryId } = getConfigurations(); - const getChildrenURL = + const getFolderByPathURL = credentials.uri + "browser/" + repositoryId + - "/root?objectID=" + - parentId + - "&cmisselector=children"; + "/root/" + + req.data[idValue] + + "?cmisselector=object"; const config = { headers: { Authorization: `Bearer ${token}` }, }; try { - const response = await axios.get(getChildrenURL, config); - return response.data.objects; + const response = await axios.get(getFolderByPathURL, config); + return response.data.properties["cmis:objectId"].value; } catch (error) { - let statusText = "An Error Occurred"; - if (error.response && error.response.statusText) { - statusText = error.response.statusText; - } - throw new Error(statusText); + return null; } } @@ -82,7 +80,6 @@ async function createFolder(req, credentials, token, attachments) { headers: headers, }; return await updateServerRequest(folderCreateURL, formData, config); - //return response; } async function createAttachment( @@ -175,7 +172,7 @@ const updateServerRequest = async ( }; module.exports = { - getChildrenForFolderId, + getFolderIdByPath, createFolder, createAttachment, deleteAttachmentOrFolder, diff --git a/lib/sdm.js b/lib/sdm.js index 0abe474..b72dfa0 100644 --- a/lib/sdm.js +++ b/lib/sdm.js @@ -1,6 +1,6 @@ const cds = require("@sap/cds/lib"); const { - getChildrenForFolderId, + getFolderIdByPath, createFolder, createAttachment, deleteAttachmentOrFolder, @@ -18,6 +18,7 @@ const { const { duplicateFileErr, duplicateDraftFileErr, + emptyFileErr, } = require("./util/messageConsts"); module.exports = class SDMAttachmentsService extends ( @@ -68,13 +69,23 @@ module.exports = class SDMAttachmentsService extends ( const folderIds = await getFolderIdForEntity(attachments, req); let parentId = ""; if (folderIds.length == 0) { - const response = await createFolder( + const folderId = await getFolderIdByPath( req, this.creds, token, attachments ); - parentId = response.data.succinctProperties["cmis:objectId"]; + if (folderId) { + parentId = folderId; + } else { + const response = await createFolder( + req, + this.creds, + token, + attachments + ); + parentId = response.data.succinctProperties["cmis:objectId"]; + } } else { parentId = folderIds[0].folderId; } @@ -173,33 +184,36 @@ module.exports = class SDMAttachmentsService extends ( const token = await fetchAccessToken(this.creds); if (req?.parentId) { await deleteFolderWithAttachments(this.creds, token, req.parentId); - } - const deletePromises = req.attachmentsToDelete.map(async (attachment) => { - const deleteAttachmentResponse = await deleteAttachmentOrFolder( - this.creds, - token, - attachment.url + } else { + const deletePromises = req.attachmentsToDelete.map( + async (attachment) => { + const deleteAttachmentResponse = await deleteAttachmentOrFolder( + this.creds, + token, + attachment.url + ); + const delData = await this.handleRequest( + deleteAttachmentResponse, + attachment.url + ); + if (delData && Object.keys(delData).length > 0) { + failedReq.push(delData.message); + Ids.push(delData.ID); + } + } ); - const delData = await this.handleRequest( - deleteAttachmentResponse, - attachment.url + // Execute all promises + await Promise.all(deletePromises); + let removeCondition = (obj) => Ids.includes(obj.ID); + req.attachmentsToDelete = req.attachmentsToDelete.filter( + (obj) => !removeCondition(obj) ); - if (delData && Object.keys(delData).length > 0) { - failedReq.push(delData.message); - Ids.push(delData.ID); - } - }); - // Execute all promises - await Promise.all(deletePromises); - let removeCondition = (obj) => Ids.includes(obj.ID); - req.attachmentsToDelete = req.attachmentsToDelete.filter( - (obj) => !removeCondition(obj) - ); - let errorResponse = ""; - failedReq.forEach((attachment) => { - errorResponse = errorResponse + "\n" + attachment; - }); - if (errorResponse != "") req.info(200, errorResponse); + let errorResponse = ""; + failedReq.forEach((attachment) => { + errorResponse = errorResponse + "\n" + attachment; + }); + if (errorResponse != "") req.info(200, errorResponse); + } } } @@ -213,9 +227,7 @@ module.exports = class SDMAttachmentsService extends ( data.map(async (d) => { // Check if d.content is null if (d.content === null) { - failedReq.push( - `Content of file ${d.filename} is empty. Either it is corrupted or not uploaded properly.` - ); + failedReq.push(emptyFileErr(d.filename)); Ids.push(d.ID); } else { const response = await createAttachment( @@ -238,16 +250,6 @@ module.exports = class SDMAttachmentsService extends ( } }) ); - if (Ids.length > 0) { - const attachmentsInFolder = await getChildrenForFolderId( - parentId, - token, - credentials - ); - if (attachmentsInFolder.length == 0) { - await deleteAttachmentOrFolder(credentials, token, parentId); - } - } let removeCondition = (obj) => Ids.includes(obj.ID); req.data.attachments = req.data.attachments.filter( diff --git a/lib/util/messageConsts.js b/lib/util/messageConsts.js index f26b9a4..eb3fa7f 100644 --- a/lib/util/messageConsts.js +++ b/lib/util/messageConsts.js @@ -2,3 +2,5 @@ module.exports.duplicateFileErr = (duplicateFiles) => `The files ${duplicateFiles} are already present in the repository. Please remove them from drafts, rename and try again.`; module.exports.duplicateDraftFileErr = (duplicateDraftFiles) => `The files ${duplicateDraftFiles} are added multiple times. Please remove them from drafts, rename and try again.`; +module.exports.emptyFileErr = (fileName) => + `Content of file ${fileName} is empty. Either it is corrupted or not uploaded properly.`; From 24ed39430cdc12f8ed1b81195beac19fc999be96 Mon Sep 17 00:00:00 2001 From: YashmeetSAP Date: Wed, 22 May 2024 14:06:18 +0530 Subject: [PATCH 3/4] Review comments & UTs --- lib/handler/index.js | 9 +- lib/persistence/index.js | 10 -- lib/sdm.js | 41 +------ lib/util/messageConsts.js | 2 - test/lib/handler/index.test.js | 210 +++++++++++++++++++++++++++------ test/lib/sdm.test.js | 128 ++++++-------------- 6 files changed, 223 insertions(+), 177 deletions(-) diff --git a/lib/handler/index.js b/lib/handler/index.js index a3b779a..dbbf8ed 100644 --- a/lib/handler/index.js +++ b/lib/handler/index.js @@ -57,6 +57,11 @@ async function getFolderIdByPath(req, credentials, token, attachments) { const response = await axios.get(getFolderByPathURL, config); return response.data.properties["cmis:objectId"].value; } catch (error) { + let statusText = "An Error Occurred"; + if (error.response && error.response.statusText) { + statusText = error.response.statusText; + } + console.log(statusText); return null; } } @@ -120,7 +125,7 @@ async function createAttachment( return response; } -async function deleteAttachmentOrFolder(credentials, token, objectId) { +async function deleteAttachmentsOfFolder(credentials, token, objectId) { const { repositoryId } = getConfigurations(); const documentDeleteURL = credentials.uri + "browser/" + repositoryId + "/root"; @@ -175,7 +180,7 @@ module.exports = { getFolderIdByPath, createFolder, createAttachment, - deleteAttachmentOrFolder, + deleteAttachmentsOfFolder, deleteFolderWithAttachments, readAttachment, }; diff --git a/lib/persistence/index.js b/lib/persistence/index.js index 53dd4d7..c972d1b 100644 --- a/lib/persistence/index.js +++ b/lib/persistence/index.js @@ -22,15 +22,6 @@ async function getFolderIdForEntity(attachments, req) { .where({ [up_]: req.data[idValue] }); } -async function getDuplicateAttachments(fileNames, attachments, req) { - const up_ = attachments.keys.up_.keys[0].$generatedFieldName; - const idValue = up_.split("__")[1]; - return await SELECT.distinct(["filename"]) - .from(attachments) - .where({ [up_]: req.data[idValue] }) - .and({ filename: { in: fileNames } }); -} - async function getURLsToDeleteFromAttachments(deletedAttachments, attachments) { return await SELECT.from(attachments) .columns("url") @@ -38,7 +29,6 @@ async function getURLsToDeleteFromAttachments(deletedAttachments, attachments) { } module.exports = { getDraftAttachments, - getDuplicateAttachments, getURLsToDeleteFromAttachments, getURLFromAttachments, getFolderIdForEntity, diff --git a/lib/sdm.js b/lib/sdm.js index b72dfa0..8612771 100644 --- a/lib/sdm.js +++ b/lib/sdm.js @@ -3,14 +3,13 @@ const { getFolderIdByPath, createFolder, createAttachment, - deleteAttachmentOrFolder, + deleteAttachmentsOfFolder, deleteFolderWithAttachments, readAttachment, } = require("../lib/handler"); const { fetchAccessToken } = require("./util/index"); const { getDraftAttachments, - getDuplicateAttachments, getURLsToDeleteFromAttachments, getURLFromAttachments, getFolderIdForEntity, @@ -55,20 +54,11 @@ module.exports = class SDMAttachmentsService extends ( if (duplicateDraftFilesErrMsg != "") { req.reject(409, duplicateDraftFileErr(duplicateDraftFilesErrMsg)); } - //verify if duplicates exist for the drafts - const duplicateFilesErrMsg = await this.isFileNameDuplicate( - attachment_val, - attachments, - req - ); - if (duplicateFilesErrMsg != "") { - req.reject(409, duplicateFileErr(duplicateFilesErrMsg)); - } const token = await fetchAccessToken(this.creds); console.log("Token: ", token); const folderIds = await getFolderIdForEntity(attachments, req); let parentId = ""; - if (folderIds.length == 0) { + if (folderIds?.length == 0) { const folderId = await getFolderIdByPath( req, this.creds, @@ -87,7 +77,7 @@ module.exports = class SDMAttachmentsService extends ( parentId = response.data.succinctProperties["cmis:objectId"]; } } else { - parentId = folderIds[0].folderId; + parentId = folderIds ? folderIds[0].folderId : ""; } const failedReq = await this.onCreate( attachment_val, @@ -121,27 +111,6 @@ module.exports = class SDMAttachmentsService extends ( return duplicates.join(", "); } - async isFileNameDuplicate(attachment_val, attachments, req) { - let fileNames = []; - for (let index in attachment_val) { - fileNames.push(attachment_val[index].filename); - } - const are_duplicates = await getDuplicateAttachments( - fileNames, - attachments, - req - ); - let duplicateFilesErrMsg = ""; - are_duplicates.forEach((file) => { - duplicateFilesErrMsg += "," + file.filename; - }); - // Remove leading comma if any - if (duplicateFilesErrMsg.charAt(0) === ",") { - duplicateFilesErrMsg = duplicateFilesErrMsg.slice(1); - } - return duplicateFilesErrMsg; - } - async attachDeletionData(req) { const attachments = cds.model.definitions[req.query.target.name + ".attachments"]; @@ -165,7 +134,7 @@ module.exports = class SDMAttachmentsService extends ( } if (req.event == "DELETE") { const folderIds = await getFolderIdForEntity(attachments, req); - if (folderIds.length > 0) { + if (folderIds?.length > 0) { const parentId = folderIds[0].folderId; req.parentId = parentId; } @@ -187,7 +156,7 @@ module.exports = class SDMAttachmentsService extends ( } else { const deletePromises = req.attachmentsToDelete.map( async (attachment) => { - const deleteAttachmentResponse = await deleteAttachmentOrFolder( + const deleteAttachmentResponse = await deleteAttachmentsOfFolder( this.creds, token, attachment.url diff --git a/lib/util/messageConsts.js b/lib/util/messageConsts.js index eb3fa7f..8b8d3bb 100644 --- a/lib/util/messageConsts.js +++ b/lib/util/messageConsts.js @@ -1,5 +1,3 @@ -module.exports.duplicateFileErr = (duplicateFiles) => - `The files ${duplicateFiles} are already present in the repository. Please remove them from drafts, rename and try again.`; module.exports.duplicateDraftFileErr = (duplicateDraftFiles) => `The files ${duplicateDraftFiles} are added multiple times. Please remove them from drafts, rename and try again.`; module.exports.emptyFileErr = (fileName) => diff --git a/test/lib/handler/index.test.js b/test/lib/handler/index.test.js index 99866d6..bd700a2 100644 --- a/test/lib/handler/index.test.js +++ b/test/lib/handler/index.test.js @@ -21,56 +21,165 @@ jest.mock("../../../lib/util/index", () => { const FormData = require("form-data"); const { getConfigurations } = require("../../../lib/util/index"); const createAttachment = require("../../../lib/handler/index").createAttachment; -const deleteAttachment = require("../../../lib/handler/index").deleteAttachment; +const deleteAttachmentsOfFolder = + require("../../../lib/handler/index").deleteAttachmentsOfFolder; const readAttachment = require("../../../lib/handler/index").readAttachment; +const getFolderIdByPath = + require("../../../lib/handler/index").getFolderIdByPath; +const createFolder = require("../../../lib/handler/index").createFolder; +const deleteFolderWithAttachments = + require("../../../lib/handler/index").deleteFolderWithAttachments; describe("handlers", () => { - describe('ReadAttachment function', () => { + describe("ReadAttachment function", () => { beforeEach(() => { jest.clearAllMocks(); }); - it('returns document on successful read', async () => { - const mockKey = '123'; - const mockToken = 'a1b2c3'; - const mockCredentials = {uri: 'http://example.com/'}; - const mockRepositoryId = '123'; - - const mockResponse = {data: 'mock pdf file content'}; - const mockBuffer = Buffer.from(mockResponse.data, 'binary'); - + it("returns document on successful read", async () => { + const mockKey = "123"; + const mockToken = "a1b2c3"; + const mockCredentials = { uri: "http://example.com/" }; + //const mockRepositoryId = "123"; + + const mockResponse = { data: "mock pdf file content" }; + const mockBuffer = Buffer.from(mockResponse.data, "binary"); + axios.get.mockResolvedValue(mockResponse); - getConfigurations.mockReturnValue({repositoryId: mockRepositoryId}); - - const document = await readAttachment(mockKey, mockToken, mockCredentials); - - const expectedUrl = mockCredentials.uri+ "browser/" + mockRepositoryId + "/root?objectID=" + mockKey + "&cmisselector=content"; + //getConfigurations.mockReturnValue({ repositoryId: mockRepositoryId }); + + const document = await readAttachment( + mockKey, + mockToken, + mockCredentials + ); + + const expectedUrl = + mockCredentials.uri + + "browser/123/root?objectID=" + + mockKey + + "&cmisselector=content"; expect(axios.get).toHaveBeenCalledWith(expectedUrl, { - headers: {Authorization: `Bearer ${mockToken}`}, - responseType: 'arraybuffer' + headers: { Authorization: `Bearer ${mockToken}` }, + responseType: "arraybuffer", }); expect(document).toEqual(mockBuffer); }); - - it('throws error on unsuccessful read', async () => { - axios.get.mockImplementationOnce(() => Promise.reject({ - response: { - statusText: 'something bad happened' - } - })); - - await expect(readAttachment('123', 'a1b2c3', {uri: 'http://example.com/'})).rejects.toThrow('something bad happened'); + + it("throws error on unsuccessful read", async () => { + axios.get.mockImplementationOnce(() => + Promise.reject({ + response: { + statusText: "something bad happened", + }, + }) + ); + + await expect( + readAttachment("123", "a1b2c3", { uri: "http://example.com/" }) + ).rejects.toThrow("something bad happened"); }); it('throws error with "An Error Occurred" message when statusText is missing', async () => { - axios.get.mockImplementationOnce(() => Promise.reject({ - response: {} - })); - - await expect(readAttachment('123', 'a1b2c3', {uri: 'http://example.com/'})).rejects.toThrow('An Error Occurred'); + axios.get.mockImplementationOnce(() => + Promise.reject({ + response: {}, + }) + ); + + await expect( + readAttachment("123", "a1b2c3", { uri: "http://example.com/" }) + ).rejects.toThrow("An Error Occurred"); + }); + }); + + describe("Test for getFolderIdByPath", () => { + let mockedReq, mockedCredentials, mockedToken, mockedAttachments; + beforeEach(() => { + jest.clearAllMocks(); + mockedReq = { data: { idValue: "testValue" } }; + mockedCredentials = { uri: "mocked_uri/" }; + mockedToken = "mocked_token"; + mockedAttachments = { + keys: { up_: { keys: [{ $generatedFieldName: "__idValue" }] } }, + }; + }); + + it("should return a folderId when axios request is success", async () => { + const mockedResponse = { + data: { properties: { "cmis:objectId": { value: "folderId" } } }, + }; + axios.get.mockResolvedValue(mockedResponse); + //getConfigurations.mockReturnValue({ repositoryId: "123" }); + + const result = await getFolderIdByPath( + mockedReq, + mockedCredentials, + mockedToken, + mockedAttachments + ); + + // assertions + expect(result).toEqual("folderId"); + expect(axios.get).toHaveBeenCalledWith( + "mocked_uri/browser/123/root/testValue?cmisselector=object", + { headers: { Authorization: "Bearer mocked_token" } } + ); + }); + + it("should return null when axios request fails", async () => { + axios.get.mockRejectedValue(new Error("Network error")); + //getConfigurations.mockReturnValue({ repositoryId: "123" }); + + const result = await getFolderIdByPath( + mockedReq, + mockedCredentials, + mockedToken, + mockedAttachments + ); + + // assertions + expect(result).toEqual(null); + expect(axios.get).toHaveBeenCalledWith( + "mocked_uri/browser/123/root/testValue?cmisselector=object", + { headers: { Authorization: "Bearer mocked_token" } } + ); + }); + }); + + describe("createFolder", () => { + it("should create a folder and return expected response when updateServerRequest is successful", async () => { + // arrange + const mockResponse = { data: "some_data" }; + axios.post.mockResolvedValue(mockResponse); + const mockedReq = { data: { field1: "value1" } }; + const mockedCredentials = { uri: "mocked_uri/" }; + const mockedToken = "mocked_token"; + const mockedAttachments = { + keys: { + up_: { + keys: [ + { + $generatedFieldName: "field1__123", + }, + ], + }, + }, + }; + // act + const response = await createFolder( + mockedReq, + mockedCredentials, + mockedToken, + mockedAttachments + ); + // assert + expect(response).toEqual(mockResponse); + expect(axios.post).toHaveBeenCalledTimes(1); + expect(axios.post).toHaveBeenCalled(); }); }); - + describe("createAttachment function", () => { beforeEach(() => { jest.clearAllMocks(); @@ -98,9 +207,10 @@ describe("handlers", () => { }); }); - describe("deleteAttachment()", () => { + describe("deleteAttachmentsOfFolder()", () => { beforeEach(() => { axios.post.mockClear(); + jest.clearAllMocks(); }); it("should perform the delete operation for given attachment", async () => { @@ -110,7 +220,7 @@ describe("handlers", () => { const objectId = "demo-objectId"; const attachments = {}; - const response = await deleteAttachment( + const response = await deleteAttachmentsOfFolder( credentials, token, objectId, @@ -134,8 +244,7 @@ describe("handlers", () => { const token = "demo-token"; const objectId = "demo-objectId"; const attachments = {}; - - const response = await deleteAttachment( + const response = await deleteAttachmentsOfFolder( credentials, token, objectId, @@ -155,4 +264,31 @@ describe("handlers", () => { ); }); }); + + describe("deleteFolderWithAttachments", () => { + beforeEach(() => { + axios.post.mockClear(); + jest.clearAllMocks(); + }); + it("should delete a folder and return expected response when updateServerRequest is successful", async () => { + // arrange + const mockResponse = { data: "some_data" }; + axios.post.mockResolvedValue(mockResponse); + const mockedCredentials = { uri: "mocked_uri/" }; + const mockedToken = "mocked_token"; + const parentId = "mocked_parentId"; + + // act + const response = await deleteFolderWithAttachments( + mockedCredentials, + mockedToken, + parentId + ); + + // assert + expect(response).toEqual(mockResponse); + expect(axios.post).toHaveBeenCalledTimes(1); + expect(axios.post).toHaveBeenCalled(); + }); + }); }); diff --git a/test/lib/sdm.test.js b/test/lib/sdm.test.js index 180e046..da19170 100644 --- a/test/lib/sdm.test.js +++ b/test/lib/sdm.test.js @@ -7,8 +7,11 @@ const getURLsToDeleteFromAttachments = require("../../lib/persistence").getURLsToDeleteFromAttachments; const getURLFromAttachments = require("../../lib/persistence").getURLFromAttachments; +const getFolderIdForEntity = + require("../../lib/persistence").getFolderIdForEntity; const fetchAccessToken = require("../../lib/util").fetchAccessToken; -const deleteAttachment = require("../../lib/handler").deleteAttachment; +const deleteAttachmentsOfFolder = + require("../../lib/handler").deleteAttachmentsOfFolder; const createAttachment = require("../../lib/handler").createAttachment; const readAttachment = require("../../lib/handler").readAttachment; const { duplicateFileErr } = require("../../lib/util/messageConsts"); @@ -18,13 +21,14 @@ jest.mock("../../lib/persistence", () => ({ getDraftAttachments: jest.fn(), getDuplicateAttachments: jest.fn(), getURLsToDeleteFromAttachments: jest.fn(), - getURLFromAttachments: jest.fn() + getURLFromAttachments: jest.fn(), + getFolderIdForEntity: jest.fn(), })); jest.mock("../../lib/util", () => ({ fetchAccessToken: jest.fn(), })); jest.mock("../../lib/handler", () => ({ - deleteAttachment: jest.fn(), + deleteAttachmentsOfFolder: jest.fn(), createAttachment: jest.fn(), readAttachment: jest.fn(), })); @@ -45,49 +49,59 @@ describe("SDMAttachmentsService", () => { service = new SDMAttachmentsService(); service.creds = { uri: "mock_cred" }; }); - + it("should interact with DB, fetch access token and readAttachment with correct parameters", async () => { const attachments = ["attachment1", "attachment2"]; const keys = ["key1", "key2"]; const token = "dummy_token"; - const response = {url:'mockUrl'} - + const response = { url: "mockUrl" }; + fetchAccessToken.mockResolvedValueOnce(token); - getURLFromAttachments.mockResolvedValueOnce(response) - + getURLFromAttachments.mockResolvedValueOnce(response); + await service.get(attachments, keys); - - expect(getURLFromAttachments).toHaveBeenCalledWith(keys,attachments) + + expect(getURLFromAttachments).toHaveBeenCalledWith(keys, attachments); expect(fetchAccessToken).toHaveBeenCalledWith(service.creds); - expect(readAttachment).toHaveBeenCalledWith("mockUrl", token, service.creds); + expect(readAttachment).toHaveBeenCalledWith( + "mockUrl", + token, + service.creds + ); }); it("should throw error if readAttachment fails", async () => { const attachments = ["attachment1", "attachment2"]; const keys = ["key1", "key2"]; const token = "dummy_token"; - const response = {url:'mockUrl'} - + const response = { url: "mockUrl" }; + fetchAccessToken.mockResolvedValueOnce(token); getURLFromAttachments.mockResolvedValueOnce(response); // Make readAttachment to throw error readAttachment.mockImplementationOnce(() => { - throw new Error('Error reading attachment'); + throw new Error("Error reading attachment"); }); - - await expect(service.get(attachments, keys)).rejects.toThrow('Error reading attachment'); - - expect(getURLFromAttachments).toHaveBeenCalledWith(keys,attachments); + + await expect(service.get(attachments, keys)).rejects.toThrow( + "Error reading attachment" + ); + + expect(getURLFromAttachments).toHaveBeenCalledWith(keys, attachments); expect(fetchAccessToken).toHaveBeenCalledWith(service.creds); - expect(readAttachment).toHaveBeenCalledWith("mockUrl", token, service.creds); + expect(readAttachment).toHaveBeenCalledWith( + "mockUrl", + token, + service.creds + ); }); - }); describe("draftSaveHandler", () => { let service; let mockReq; let cds; beforeEach(() => { + jest.clearAllMocks(); cds = require("@sap/cds/lib"); service = new SDMAttachmentsService(); service.creds = { uaa: "mocked uaa" }; @@ -110,17 +124,6 @@ describe("SDMAttachmentsService", () => { }; }); - it("should reject if duplicates exist", async () => { - getDraftAttachments.mockResolvedValue([{ id: 1 }, { id: 2 }]); - service.onCreate = jest.fn().mockResolvedValue([]); - service.isFileNameDuplicate = jest.fn().mockResolvedValue("error"); - await service.draftSaveHandler(mockReq); - expect(mockReq.reject).toHaveBeenCalledWith( - 409, - duplicateFileErr("error") - ); - }); - it("should handle failure in onCreate", async () => { service.isFileNameDuplicate = jest.fn().mockResolvedValue(""); service.onCreate = jest.fn().mockResolvedValue(["ChildTest"]); @@ -151,61 +154,6 @@ describe("SDMAttachmentsService", () => { }); }); - describe("isFileNameDuplicate", () => { - let service; - beforeEach(() => { - service = new SDMAttachmentsService(); - }); - - it("should detect duplicates", async () => { - const attachments = [ - /* array of attachment objects */ - ]; - const attachment_val = [{ filename: "file1" }, { filename: "file2" }]; - getDuplicateAttachments.mockResolvedValue([{ filename: "file1" }]); - - const result = await service.isFileNameDuplicate( - attachment_val, - attachments - ); - - expect(result).toBe("file1"); - }); - - it("should return empty string if no duplicates", async () => { - const attachments = [ - /* array of attachment objects */ - ]; - const attachment_val = [{ filename: "file1" }, { filename: "file2" }]; - getDuplicateAttachments.mockResolvedValue([]); - - const result = await service.isFileNameDuplicate( - attachment_val, - attachments - ); - - expect(result).toBe(""); - }); - - it("should concatenate multiple duplicates", async () => { - const attachments = [ - /* array of attachment objects */ - ]; - const attachment_val = [{ filename: "file1" }, { filename: "file2" }]; - getDuplicateAttachments.mockResolvedValue([ - { filename: "file1" }, - { filename: "file2" }, - ]); - - const result = await service.isFileNameDuplicate( - attachment_val, - attachments - ); - - expect(result).toBe("file1,file2"); - }); - }); - describe("attachDeletionData", () => { let service; beforeEach(() => { @@ -312,7 +260,7 @@ describe("SDMAttachmentsService", () => { cds.model.definitions["testTarget.attachments"] = {}; // Add relevant attachment definition fetchAccessToken.mockResolvedValue("test_token"); - deleteAttachment.mockResolvedValue({}); + deleteAttachmentsOfFolder.mockResolvedValue({}); service.handleRequest = jest .fn() .mockResolvedValueOnce({ message: expectedErrorResponse, ID: "2" }); @@ -320,14 +268,14 @@ describe("SDMAttachmentsService", () => { await service.deleteAttachmentsWithKeys(records, req); expect(fetchAccessToken).toHaveBeenCalledTimes(1); - expect(deleteAttachment).toHaveBeenCalledTimes(2); + expect(deleteAttachmentsOfFolder).toHaveBeenCalledTimes(2); expect(service.handleRequest).toHaveBeenCalledTimes(2); expect(req.attachmentsToDelete).toHaveLength(1); expect(req.attachmentsToDelete[0].ID).toEqual("1"); expect(req.info).toHaveBeenCalledWith(200, "\n" + expectedErrorResponse); }); - it("should not call fetchAccessToken, deleteAttachment, and handleRequest methods if req.attachmentsToDelete is empty", async () => { + it("should not call fetchAccessToken, deleteAttachmentsOfFolder, and handleRequest methods if req.attachmentsToDelete is empty", async () => { const records = []; jest.spyOn(service, "handleRequest"); const req = { @@ -338,7 +286,7 @@ describe("SDMAttachmentsService", () => { await service.deleteAttachmentsWithKeys(records, req); expect(fetchAccessToken).not.toHaveBeenCalled(); - expect(deleteAttachment).not.toHaveBeenCalled(); + expect(deleteAttachmentsOfFolder).not.toHaveBeenCalled(); expect(service.handleRequest).not.toHaveBeenCalled(); }); }); From 4b8a7ceb87a373f253a3b97d69c5d2acf8bab601 Mon Sep 17 00:00:00 2001 From: YashmeetSAP Date: Wed, 22 May 2024 17:07:47 +0530 Subject: [PATCH 4/4] UTs --- lib/sdm.js | 6 +- test/lib/handler/index.test.js | 59 ++++++-- test/lib/sdm.test.js | 258 +++++++++++++++++++++++++++++++-- test/lib/util/index.test.js | 6 +- 4 files changed, 293 insertions(+), 36 deletions(-) diff --git a/lib/sdm.js b/lib/sdm.js index 8612771..7506212 100644 --- a/lib/sdm.js +++ b/lib/sdm.js @@ -14,11 +14,7 @@ const { getURLFromAttachments, getFolderIdForEntity, } = require("../lib/persistence"); -const { - duplicateFileErr, - duplicateDraftFileErr, - emptyFileErr, -} = require("./util/messageConsts"); +const { duplicateDraftFileErr, emptyFileErr } = require("./util/messageConsts"); module.exports = class SDMAttachmentsService extends ( require("@cap-js/attachments/lib/basic") diff --git a/test/lib/handler/index.test.js b/test/lib/handler/index.test.js index bd700a2..971aa81 100644 --- a/test/lib/handler/index.test.js +++ b/test/lib/handler/index.test.js @@ -20,15 +20,14 @@ jest.mock("../../../lib/util/index", () => { }); const FormData = require("form-data"); const { getConfigurations } = require("../../../lib/util/index"); -const createAttachment = require("../../../lib/handler/index").createAttachment; -const deleteAttachmentsOfFolder = - require("../../../lib/handler/index").deleteAttachmentsOfFolder; -const readAttachment = require("../../../lib/handler/index").readAttachment; -const getFolderIdByPath = - require("../../../lib/handler/index").getFolderIdByPath; -const createFolder = require("../../../lib/handler/index").createFolder; -const deleteFolderWithAttachments = - require("../../../lib/handler/index").deleteFolderWithAttachments; +const { + createAttachment, + deleteAttachmentsOfFolder, + readAttachment, + getFolderIdByPath, + createFolder, + deleteFolderWithAttachments, +} = require("../../../lib/handler/index"); describe("handlers", () => { describe("ReadAttachment function", () => { @@ -40,13 +39,11 @@ describe("handlers", () => { const mockKey = "123"; const mockToken = "a1b2c3"; const mockCredentials = { uri: "http://example.com/" }; - //const mockRepositoryId = "123"; const mockResponse = { data: "mock pdf file content" }; const mockBuffer = Buffer.from(mockResponse.data, "binary"); axios.get.mockResolvedValue(mockResponse); - //getConfigurations.mockReturnValue({ repositoryId: mockRepositoryId }); const document = await readAttachment( mockKey, @@ -110,7 +107,6 @@ describe("handlers", () => { data: { properties: { "cmis:objectId": { value: "folderId" } } }, }; axios.get.mockResolvedValue(mockedResponse); - //getConfigurations.mockReturnValue({ repositoryId: "123" }); const result = await getFolderIdByPath( mockedReq, @@ -129,7 +125,6 @@ describe("handlers", () => { it("should return null when axios request fails", async () => { axios.get.mockRejectedValue(new Error("Network error")); - //getConfigurations.mockReturnValue({ repositoryId: "123" }); const result = await getFolderIdByPath( mockedReq, @@ -145,6 +140,44 @@ describe("handlers", () => { { headers: { Authorization: "Bearer mocked_token" } } ); }); + + it("should log statusText and return null when axios.get throws an error with response.statusText", async () => { + // create the mock objects + const mockedReq = { data: { field1: "value1" } }; + const mockedCredentials = { uri: "mocked_uri/" }; + const mockedToken = "mocked_token"; + const mockedAttachments = { + keys: { + up_: { + keys: [ + { + $generatedFieldName: "field1__123", + }, + ], + }, + }, + }; + const errorResponse = { statusText: "Some error occurred" }; + axios.get.mockRejectedValue({ response: errorResponse }); + + // spy on console.log + const logSpy = jest.spyOn(console, "log"); + + // call the function + const response = await getFolderIdByPath( + mockedReq, + mockedCredentials, + mockedToken, + mockedAttachments + ); + + // assert that the function returned null and printed the statusText + expect(response).toBeNull(); + expect(logSpy).toHaveBeenCalledWith("Some error occurred"); + + // restore console.log + logSpy.mockRestore(); + }); }); describe("createFolder", () => { diff --git a/test/lib/sdm.test.js b/test/lib/sdm.test.js index da19170..8333eac 100644 --- a/test/lib/sdm.test.js +++ b/test/lib/sdm.test.js @@ -1,20 +1,23 @@ const SDMAttachmentsService = require("../../lib/sdm"); -const getDraftAttachments = - require("../../lib/persistence").getDraftAttachments; -const getDuplicateAttachments = - require("../../lib/persistence").getDuplicateAttachments; -const getURLsToDeleteFromAttachments = - require("../../lib/persistence").getURLsToDeleteFromAttachments; -const getURLFromAttachments = - require("../../lib/persistence").getURLFromAttachments; -const getFolderIdForEntity = - require("../../lib/persistence").getFolderIdForEntity; const fetchAccessToken = require("../../lib/util").fetchAccessToken; -const deleteAttachmentsOfFolder = - require("../../lib/handler").deleteAttachmentsOfFolder; -const createAttachment = require("../../lib/handler").createAttachment; -const readAttachment = require("../../lib/handler").readAttachment; -const { duplicateFileErr } = require("../../lib/util/messageConsts"); +const { + getDraftAttachments, + getURLsToDeleteFromAttachments, + getURLFromAttachments, + getFolderIdForEntity, +} = require("../../lib/persistence"); +const { + deleteAttachmentsOfFolder, + createAttachment, + readAttachment, + getFolderIdByPath, + createFolder, + deleteFolderWithAttachments, +} = require("../../lib/handler"); +const { + duplicateDraftFileErr, + emptyFileErr, +} = require("../../lib/util/messageConsts"); jest.mock("@cap-js/attachments/lib/basic", () => class {}); jest.mock("../../lib/persistence", () => ({ @@ -31,6 +34,9 @@ jest.mock("../../lib/handler", () => ({ deleteAttachmentsOfFolder: jest.fn(), createAttachment: jest.fn(), readAttachment: jest.fn(), + getFolderIdByPath: jest.fn(), + createFolder: jest.fn(), + deleteFolderWithAttachments: jest.fn(), })); jest.mock("@sap/cds/lib", () => { const mockCds = { @@ -124,6 +130,66 @@ describe("SDMAttachmentsService", () => { }; }); + it("draftSaveHandler() should do nothing when getDraftAttachments() returns empty array", async () => { + getDraftAttachments.mockResolvedValueOnce([]); + await service.draftSaveHandler(mockReq); + expect(getDraftAttachments).toHaveBeenCalledTimes(1); + expect(fetchAccessToken).toHaveBeenCalledTimes(0); + expect(getFolderIdForEntity).toHaveBeenCalledTimes(0); + }); + + it("draftSaveHandler() should call getFolderIdByPath if getFolderIdForEntity returns empty array", async () => { + getDraftAttachments.mockResolvedValueOnce(["file1", "file2"]); + service.isFileNameDuplicateInDrafts = jest.fn().mockReturnValueOnce(""); + fetchAccessToken.mockResolvedValueOnce("mocked_token"); + getFolderIdForEntity.mockResolvedValueOnce([]); + getFolderIdByPath.mockResolvedValueOnce("mocked_folder_id"); + service.onCreate = jest.fn().mockResolvedValueOnce([]); + await service.draftSaveHandler(mockReq); + expect(getDraftAttachments).toHaveBeenCalledTimes(1); + expect(service.isFileNameDuplicateInDrafts).toHaveBeenCalledTimes(1); + expect(fetchAccessToken).toHaveBeenCalledWith(service.creds); + expect(getFolderIdForEntity).toHaveBeenCalledTimes(1); + expect(getFolderIdByPath).toHaveBeenCalledWith( + mockReq, + service.creds, + "mocked_token", + cds.model.definitions[mockReq.query.target.name + ".attachments"] + ); + }); + + it("draftSaveHandler() should call createFolder if getFolderIdForEntity and getFolderIdByPath return empty", async () => { + const attachments = ["file1", "file2"]; + getDraftAttachments.mockResolvedValueOnce(attachments); + service.isFileNameDuplicateInDrafts = jest.fn().mockReturnValueOnce(""); + fetchAccessToken.mockResolvedValueOnce("mocked_token"); + getFolderIdForEntity.mockResolvedValueOnce([]); + getFolderIdByPath.mockResolvedValueOnce(null); + createFolder.mockResolvedValueOnce({ + data: { + succinctProperties: { + "cmis:objectId": "new_folder_id", + }, + }, + }); + service.onCreate = jest.fn().mockResolvedValueOnce([]); + await service.draftSaveHandler(mockReq); + expect(createFolder).toHaveBeenCalledWith( + mockReq, + service.creds, + "mocked_token", + cds.model.definitions[mockReq.query.target.name + ".attachments"] + ); + expect(service.onCreate).toHaveBeenCalledWith( + attachments, + service.creds, + "mocked_token", + cds.model.definitions[mockReq.query.target.name + ".attachments"], + mockReq, + "new_folder_id" + ); + }); + it("should handle failure in onCreate", async () => { service.isFileNameDuplicate = jest.fn().mockResolvedValue(""); service.onCreate = jest.fn().mockResolvedValue(["ChildTest"]); @@ -152,6 +218,38 @@ describe("SDMAttachmentsService", () => { expect(mockReq.info).not.toBeCalled(); }); + + it("should reject if duplicate draft files exist", async () => { + const duplicateErrMsg = "Duplicate Error Message"; + const mockAttachments = [ + { name: "Attachment#1" }, + { name: "Attachment#2" }, + ]; + + // Mock method return values + getDraftAttachments.mockResolvedValue(mockAttachments); + service.isFileNameDuplicateInDrafts = jest + .fn() + .mockReturnValue(duplicateErrMsg); + service.onCreate = jest + .fn() + .mockResolvedValue(["Failed request 1", "Failed request 2"]); + + // Method under test + await service.draftSaveHandler(mockReq); + + // Assert mockReq.reject was called with the right arguments + expect(mockReq.reject).toHaveBeenCalledWith( + 409, + duplicateDraftFileErr(duplicateErrMsg) + ); + + // verify if getDraftAttachments was called + expect(getDraftAttachments).toHaveBeenCalledWith( + expect.anything(), + mockReq + ); + }); }); describe("attachDeletionData", () => { @@ -237,9 +335,78 @@ describe("SDMAttachmentsService", () => { expect(getURLsToDeleteFromAttachments).not.toHaveBeenCalled(); expect(mockedReq.attachmentsToDelete).toBeUndefined(); }); + + it("attachDeletionData() should set req.parentId if event is DELETE and getFolderIdForEntity() returns non-empty array", async () => { + const mockReq = { + query: { target: { name: "testName" } }, + diff: () => + Promise.resolve({ attachments: [{ _op: "delete", ID: "1" }] }), + event: "DELETE", + }; + getURLsToDeleteFromAttachments.mockResolvedValueOnce(["url"]); + getFolderIdForEntity.mockResolvedValueOnce([{ folderId: "folder" }]); + await service.attachDeletionData(mockReq); + expect(mockReq.parentId).toEqual("folder"); + expect(getFolderIdForEntity).toHaveBeenCalledTimes(1); + }); + + it("attachDeletionData() should not set req.parentId if event is DELETE and getFolderIdForEntity() returns empty array", async () => { + const mockReq = { + query: { target: { name: "testName" } }, + diff: () => + Promise.resolve({ attachments: [{ _op: "delete", ID: "1" }] }), + event: "DELETE", + }; + getURLsToDeleteFromAttachments.mockResolvedValueOnce(["url"]); + getFolderIdForEntity.mockResolvedValueOnce([]); + await service.attachDeletionData(mockReq); + expect(mockReq.parentId).toBeUndefined(); + expect(getFolderIdForEntity).toHaveBeenCalledTimes(1); + }); + + it("attachDeletionData() should not call getFolderIdForEntity() if event is not DELETE", async () => { + const mockReq = { + query: { target: { name: "testName" } }, + diff: () => + Promise.resolve({ attachments: [{ _op: "delete", ID: "1" }] }), + event: "CREATE", + }; + getURLsToDeleteFromAttachments.mockResolvedValueOnce(["url"]); + await service.attachDeletionData(mockReq); + expect(getFolderIdForEntity).toHaveBeenCalledTimes(0); + }); + it("attachDeletionData() should not proceed if attachments are not defined", async () => { + const mockReq = { + query: { + target: { name: "testName" }, + }, + diff: jest + .fn() + .mockResolvedValueOnce({ attachments: [{ _op: "delete", ID: "1" }] }), + }; + // delete the attachments in the definitions + delete cds.model.definitions[mockReq.query.target.name + ".attachments"]; + + await service.attachDeletionData(mockReq); + + // Assuming that these are called inside if(attachments) block + expect(mockReq.diff).not.toHaveBeenCalled(); + expect(getURLsToDeleteFromAttachments).not.toHaveBeenCalled(); + }); + + it("attachDeletionData() should not set req.attachmentsToDelete if there are no attachments to delete", async () => { + const mockReq = { + query: { target: { name: "testName" } }, + diff: () => + Promise.resolve({ attachments: [{ _op: "delete", ID: "1" }] }), + }; + getURLsToDeleteFromAttachments.mockResolvedValueOnce([]); // returning empty array + await service.attachDeletionData(mockReq); + expect(mockReq.attachmentsToDelete).toBeUndefined(); + }); }); - describe("attachDeletionData", () => { + describe("deleteAttachmentsWithKeys", () => { let service; beforeEach(() => { jest.clearAllMocks(); @@ -289,6 +456,27 @@ describe("SDMAttachmentsService", () => { expect(deleteAttachmentsOfFolder).not.toHaveBeenCalled(); expect(service.handleRequest).not.toHaveBeenCalled(); }); + + test("deleteAttachmentsWithKeys() should delete entire folder when parentId is available", async () => { + const mockReq = { + query: { target: { name: "testName" } }, + attachmentsToDelete: ["file1", "file2"], + parentId: "some_folder_id", + }; + + fetchAccessToken.mockResolvedValueOnce("mocked_token"); + deleteFolderWithAttachments.mockResolvedValueOnce({}); + + await service.deleteAttachmentsWithKeys([], mockReq); + + expect(fetchAccessToken).toHaveBeenCalledWith(service.creds); + expect(deleteFolderWithAttachments).toHaveBeenCalledWith( + service.creds, + "mocked_token", + mockReq.parentId + ); + expect(deleteAttachmentsOfFolder).not.toHaveBeenCalled(); + }); }); describe("onCreate", () => { @@ -349,6 +537,44 @@ describe("SDMAttachmentsService", () => { expect(result).toEqual(["Attachment failed"]); expect(req.data.attachments).toHaveLength(1); }); + + it("onCreate() should add error message to failedReq if d.content is null", async () => { + const mockAttachments = [ + { content: null, filename: "filename1", ID: "id1" }, + { content: "valid_data", filename: "filename2", ID: "id2" }, + ]; + const mockReq = { + data: { attachments: [...mockAttachments] }, + }; + const token = "mocked_token"; + const credentials = "mocked_credentials"; + const attachments = "mocked_attachments"; + const parentId = "mocked_parentId"; + + createAttachment.mockResolvedValue({ + status: 201, + data: { succinctProperties: { "cmis:objectId": "some_object_id" } }, + }); + + const failedFiles = await service.onCreate( + mockAttachments, + credentials, + token, + attachments, + mockReq, + parentId + ); + + expect(failedFiles).toEqual([emptyFileErr("filename1")]); + expect(mockReq.data.attachments).toHaveLength(1); + expect(mockReq.data.attachments[0]).toEqual({ + content: null, + filename: "filename2", + ID: "id2", + folderId: parentId, + url: "some_object_id", + }); + }); }); describe("handleRequest", () => { diff --git a/test/lib/util/index.test.js b/test/lib/util/index.test.js index f0ae64c..90f5ed1 100644 --- a/test/lib/util/index.test.js +++ b/test/lib/util/index.test.js @@ -1,8 +1,10 @@ const xssec = require("@sap/xssec"); const NodeCache = require("node-cache"); -const fetchAccessToken = require("../../../lib/util/index").fetchAccessToken; +const { + fetchAccessToken, + getConfigurations, +} = require("../../../lib/util/index"); const cds = require("@sap/cds"); -const getConfigurations = require("../../../lib/util/index").getConfigurations; jest.mock("@sap/xssec"); jest.mock("node-cache");