diff --git a/README.md b/README.md index 79fd3a4..75b63cb 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ Create a SAP Document Management Integration Option [Service instance and key](h 6. The `Attachments` type has generated an out-of-the-box Attachments table (see highlighted box) at the bottom of the Object page: Attachments Table -7. **Upload a file** by going into Edit mode and either using the **Upload** button on the Attachments table or by drag/drop. Then click the **Save** button to have that file stored in SAP Document Management Integration Option. We demonstrate this by uploading the PDF file from [_xmpl/db/content/Solar Panel Report.pdf_](./xmpl/db/content/Solar%20Panel%20Report.pdf): +7. **Upload a file** by going into Edit mode and either using the **Upload** button on the Attachments table or by drag/drop. The file is then stored in SAP Document Management Integration Option. We demonstrate this by uploading the PDF file from [_xmpl/db/content/Solar Panel Report.pdf_](./xmpl/db/content/Solar%20Panel%20Report.pdf): Upload an attachment 8. **Open a file** by clicking on the attachment. We demonstrate this by opening the previously uploaded PDF file: `Solar Panel Report.pdf` diff --git a/lib/handler/index.js b/lib/handler/index.js index f9438f9..7e0c11a 100644 --- a/lib/handler/index.js +++ b/lib/handler/index.js @@ -1,6 +1,7 @@ const { getConfigurations } = require("../util"); const axios = require("axios").default; const FormData = require("form-data"); +const { errorMessage } = require("../util/messageConsts"); async function readAttachment(Key, token, credentials) { const document = await readDocument(Key, token, credentials.uri); @@ -70,7 +71,7 @@ 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"; + let statusText = errorMessage; if (error.response?.statusText) { statusText = error.response.statusText; } @@ -80,14 +81,13 @@ async function getFolderIdByPath(req, credentials, token, attachments) { } async function createFolder(req, credentials, token, attachments) { - const up_ = attachments.keys.up_.keys[0].$generatedFieldName; - const idValue = up_.split("__")[1]; + const upID = attachments.keys.up_.keys[0].$generatedFieldName; 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("propertyValue[0]", req.data[upID]); formData.append("propertyId[1]", "cmis:objectTypeId"); formData.append("propertyValue[1]", "cmis:folder"); formData.append("succinct", "true"); @@ -104,7 +104,6 @@ async function createAttachment( data, credentials, token, - attachments, parentId ) { const { repositoryId } = getConfigurations(); @@ -171,6 +170,32 @@ async function deleteFolderWithAttachments(credentials, token, parentId) { return response; } +async function getAttachment(uri, token, objectId) { + const { repositoryId } = getConfigurations(); + const getAttachmentURL = + uri + + "browser/" + + repositoryId + + "/root?" + + "cmisselector=object&objectId=" + + objectId + + "&succinct=true"; + + const config = { + headers: { Authorization: `Bearer ${token}` }, + }; + try { + return await axios.get(getAttachmentURL, config); + } catch (error) { + let statusText = errorMessage; + if (error.response?.statusText) { + statusText = error.response.statusText; + } + console.log(statusText); + return null; + } +} + async function renameAttachment( modifiedAttachment, credentials, @@ -215,6 +240,7 @@ module.exports = { createAttachment, deleteAttachmentsOfFolder, deleteFolderWithAttachments, + getAttachment, readAttachment, renameAttachment }; diff --git a/lib/persistence/index.js b/lib/persistence/index.js index c78d8cd..d3b99a9 100644 --- a/lib/persistence/index.js +++ b/lib/persistence/index.js @@ -17,11 +17,21 @@ async function getDraftAttachments(attachments, req, repositoryId) { .where(conditions) } +async function getDraftAttachmentsForUpID(attachments, req, repositoryId) { + const up_ = attachments.keys.up_.keys[0].$generatedFieldName; + const conditions = { + [up_]: req.data[up_], + repositoryId: repositoryId + }; + return await SELECT("filename", "mimeType", "content", "url", "ID", "HasActiveEntity") + .from(attachments) + .where(conditions) +} + async function getFolderIdForEntity(attachments, req, repositoryId) { const up_ = attachments.keys.up_.keys[0].$generatedFieldName; - const idValue = up_.split("__")[1]; const conditions = { - [up_]: req.data[idValue], + [up_]: req.data[up_], repositoryId: repositoryId }; return await SELECT.from(attachments) @@ -29,6 +39,14 @@ async function getFolderIdForEntity(attachments, req, repositoryId) { .where(conditions); } +async function updateAttachmentInDraft(req, data) { + const up_ = req.target.keys.up_.keys[0].$generatedFieldName; + const idValue = up_.split("__")[1]; + return await UPDATE(req.target) + .set({ folderId: data.folderId, url: data.url, status: "Clean" }) + .where({ [idValue]: req.data[idValue] }); +} + async function getURLsToDeleteFromAttachments(deletedAttachments, attachments) { return await SELECT.from(attachments) .columns("url") @@ -36,35 +54,37 @@ async function getURLsToDeleteFromAttachments(deletedAttachments, attachments) { } async function getExistingAttachments(attachmentIDs, attachments) { - return await SELECT("filename", "url", "ID","folderId") + return await SELECT("filename", "url", "ID", "folderId") .from(attachments) .where({ ID: { in: [...attachmentIDs] }}); } async function setRepositoryId(attachments, repositoryId) { - if(attachments){ + if(attachments) { let nullAttachments = await SELECT() .from(attachments) .where({ repositoryId: null }); - if (!nullAttachments || nullAttachments.length === 0) { - return; - } + if (!nullAttachments || nullAttachments.length === 0) { + return; + } - for (let attachment of nullAttachments) { - await UPDATE(attachments) - .set({ repositoryId: repositoryId }) - .where({ ID: attachment.ID }); - } + for (let attachment of nullAttachments) { + await UPDATE(attachments) + .set({ repositoryId: repositoryId }) + .where({ ID: attachment.ID }); + } } } module.exports = { getDraftAttachments, + getDraftAttachmentsForUpID, getURLsToDeleteFromAttachments, getURLFromAttachments, getFolderIdForEntity, + updateAttachmentInDraft, getExistingAttachments, setRepositoryId }; diff --git a/lib/sdm.js b/lib/sdm.js index 70b14c5..b20b7f9 100644 --- a/lib/sdm.js +++ b/lib/sdm.js @@ -8,6 +8,7 @@ const { createAttachment, deleteAttachmentsOfFolder, deleteFolderWithAttachments, + getAttachment, readAttachment, renameAttachment } = require("./handler/index"); @@ -20,12 +21,14 @@ const { } = require("./util/index"); const { getDraftAttachments, + getDraftAttachmentsForUpID, getURLsToDeleteFromAttachments, getURLFromAttachments, getFolderIdForEntity, + updateAttachmentInDraft, setRepositoryId } = require("../lib/persistence"); -const { duplicateDraftFileErr, emptyFileErr, renameFileErr, virusFileErr, duplicateFileErr, versionedRepositoryErr, otherFileErr } = require("./util/messageConsts"); +const { duplicateDraftFileErr, renameFileErr, virusFileErr, duplicateFileErr, versionedRepositoryErr, otherFileErr, userDoesNotHaveScopeMessage, userNotAuthorisedError } = require("./util/messageConsts"); module.exports = class SDMAttachmentsService extends ( require("@cap-js/attachments/lib/basic") @@ -49,7 +52,7 @@ module.exports = class SDMAttachmentsService extends ( const repoInfo = await getRepositoryInfo(this.creds, token); isVersioned = await isRepositoryVersioned(repoInfo, repositoryId); } else { - isVersioned = repotype == "versioned" ? true : false; + isVersioned = repotype == "versioned"; } if (isVersioned) { req.reject(400, versionedRepositoryErr); @@ -57,7 +60,6 @@ module.exports = class SDMAttachmentsService extends ( } async get(attachments, keys, req) { - await this.checkRepositoryType(req); const response = await getURLFromAttachments(keys, attachments); const token = await fetchAccessToken( this.creds, @@ -68,38 +70,82 @@ module.exports = class SDMAttachmentsService extends ( return content; } - async draftSaveHandler(req) { + async renameHandler(req) { const { repositoryId } = getConfigurations(); await this.checkRepositoryType(req); const attachments = cds.model.definitions[req.query.target.name + ".attachments"]; const attachment_val = await getDraftAttachments(attachments, req, repositoryId); if (attachment_val.length > 0) { - await this.isFileNameDuplicateInDrafts(attachment_val,req); + await this.isFileNameDuplicateInDrafts(attachment_val, req); const token = await fetchAccessToken( this.creds, req.user.tokenInfo.getTokenValue() ); - const attachment_val_rename = attachment_val.filter(attachment => attachment.HasActiveEntity === true); - const attachment_val_create = attachment_val.filter(attachment => attachment.HasActiveEntity === false); + let attachment_val_rename = []; + let draft_attachments = []; + draft_attachments = attachment_val.filter(attachment => attachment.HasActiveEntity === false); + attachment_val_rename = attachment_val.filter(attachment => attachment.HasActiveEntity === true); + const attachmentIDs = attachment_val_rename.map(attachment => attachment.ID); let modifiedAttachments = []; modifiedAttachments = await checkAttachmentsToRename(attachment_val_rename, attachmentIDs, attachments) + + draft_attachments.forEach( attachment => { + const filenameInDraft = attachment.filename; + const objectId = attachment.url; + const attachmentData = this.getAttachementDataInSDM(this.creds.uri, token, objectId); + const filenameInSDM = attachmentData.filename; + if(filenameInDraft !== filenameInSDM){ + modifiedAttachments.push({ID:attachment.ID, url: attachment.url, name: filenameInDraft, prevname: filenameInSDM, folderId:attachmentData.folderId}); + } + }); + let errorMessage = "" if(modifiedAttachments.length>0){ errorMessage = await this.rename(modifiedAttachments, token, req) } - if(attachment_val_create.length>0){ - errorMessage = await this.create(attachment_val_create, attachments, req, token, errorMessage) - } + if(errorMessage.length != 0){ req.warn(500, errorMessage); } } } + async getAttachementDataInSDM(uri, token, objectId) { + const response = await getAttachment(uri, token, objectId); + const responseData = { filename: response?.data?.succinctProperties["cmis:name"], folderId: response?.data?.succinctProperties["sap:parentIds"][0] }; + return responseData; + } + + async draftSaveHandler(req) { + if (req?.data?.content) { + const { repositoryId } = getConfigurations(); + await this.checkRepositoryType(req); + const draftAttachments = req.target; + const attachment_val = await getDraftAttachmentsForUpID(draftAttachments, req, repositoryId); + + if (attachment_val.length > 0) { + await this.isFileNameDuplicateInDrafts(attachment_val,req); + const token = await fetchAccessToken( + this.creds, + req.user.tokenInfo.getTokenValue() + ); + let attachment_val_create = []; + if (req.data.content) { + attachment_val_create = attachment_val.filter(attachment => attachment.HasActiveEntity === false && attachment.ID === req.data.ID); + } + if(attachment_val_create.length>0){ + attachment_val_create[0].content = req.data.content; + await this.create(attachment_val_create, draftAttachments, req, token) + } + } + req.data.content = null; + } + } + async rename(modifiedAttachments, token, req){ await this.checkRepositoryType(req); const failedReq = await this.onRename( @@ -115,55 +161,28 @@ module.exports = class SDMAttachmentsService extends ( return errorResponse; } - async create(attachment_val_create, attachments, req, token, renameError){ - let parentId = await this.getParentId(attachments,req,token) - const failedReq = await this.onCreate( + async create(attachment_val_create, attachments, req, token){ + let parentId = await this.getParentId(attachments, req, token) + await this.onCreate( attachment_val_create, this.creds, token, - attachments, req, parentId ); - let errorResponse; - if (renameError){ - errorResponse = renameError; - } - else{ - errorResponse = "" - } - let virusFiles = []; - let duplicateFiles = []; - let otherFiles = []; - failedReq.forEach((attachment) => { - if (attachment.typeOfError == 'virus') { - virusFiles.push(attachment.name) - } - else if (attachment.typeOfError == 'duplicate') { - duplicateFiles.push(attachment.name) - } - else{ - otherFiles.push(attachment.message); - } - }); - if(virusFiles.length != 0){ - errorResponse = errorResponse + virusFileErr(virusFiles) - } - if(duplicateFiles.length != 0){ - errorResponse = errorResponse + duplicateFileErr(duplicateFiles); - } - if(otherFiles.length != 0){ - errorResponse = errorResponse + otherFileErr(otherFiles); - } - return errorResponse; } - async getParentId(attachments,req,token){ const { repositoryId } = getConfigurations(); const folderIds = await getFolderIdForEntity(attachments, req, repositoryId); - let parentId = ""; - if (folderIds?.length == 0) { + let parentId = null; + for (const folder of folderIds) { + if (folder.folderId !== null) { + parentId = folder.folderId; + break; + } + } + if (!parentId) { const folderId = await getFolderIdByPath( req, this.creds, @@ -179,15 +198,16 @@ module.exports = class SDMAttachmentsService extends ( token, attachments ); + if (response.status == 403 && response.response.data == userDoesNotHaveScopeMessage) { + req.reject(403, userNotAuthorisedError); + } parentId = response.data.succinctProperties["cmis:objectId"]; } - } else { - parentId = folderIds ? folderIds[0].folderId : ""; } return parentId; } - async isFileNameDuplicateInDrafts(data,req) { + async isFileNameDuplicateInDrafts(data, req) { let fileNames = []; for (let index in data) { fileNames.push(data[index].filename); @@ -318,63 +338,37 @@ module.exports = class SDMAttachmentsService extends ( } } - async onCreate(data, credentials, token, attachments, req, parentId) { - let failedReq = [], - Ids = [], - success = [], - success_ids = []; + async onCreate(data, credentials, token, req, parentId) { + let fileNames = []; const { repositoryId } = getConfigurations(); await Promise.all( data.map(async (d) => { - // Check if d.content is null - if (d.content === null) { - failedReq.push(emptyFileErr(d.filename)); - Ids.push(d.ID); + const response = await createAttachment( + d, + credentials, + token, + parentId + ); + if (response.status == 201) { + d.folderId = parentId; + d.url = response.data?.succinctProperties["cmis:objectId"]; + d.repositoryId = repositoryId; + d.content = null; + await updateAttachmentInDraft(req, d); } 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.repositoryId = repositoryId; - d.content = null; - success_ids.push(d.ID); - success.push(d); - } else { - Ids.push(d.ID); - if(response.response.data.message == 'Malware Service Exception: Virus found in the file!'){ - failedReq.push({typeOfError:'virus',name:d.filename}) - } - else if(response.response.data.exception == "nameConstraintViolation"){ - failedReq.push({typeOfError:'duplicate',name:d.filename}); - } - else{ - failedReq.push({typeOfError:'other',message:response.response.data.message}); - } + fileNames.push(d.filename); + if(response.response.data.message == 'Malware Service Exception: Virus found in the file!'){ + req.reject(virusFileErr(fileNames)); + } + else if(response.response.data.exception == "nameConstraintViolation"){ + req.reject(duplicateFileErr(fileNames)); + } + else{ + req.reject(otherFileErr(fileNames)); } } }) ); - - let removeCondition = (obj) => Ids.includes(obj.ID); - req.data.attachments = req.data.attachments.filter( - (obj) => !removeCondition(obj) - ); - let removeSuccessAttachments = (obj) => success_ids.includes(obj.ID); - - // Filter out successful attachments - req.data.attachments = req.data.attachments.filter( - (obj) => !removeSuccessAttachments(obj) - ); - - // Add successful attachments to the end of the attachments array - req.data.attachments = [...req.data.attachments, ...success]; - return failedReq; } async onRename(modifiedAttachments, credentials, token, req) { @@ -438,9 +432,16 @@ module.exports = class SDMAttachmentsService extends ( entity, this.attachDeletionData.bind(this) ); - srv.before("READ", [target,target.drafts], this.setRepository.bind(this)) - srv.before("READ", [target,target.drafts], this.filterAttachments.bind(this)) - srv.before("SAVE", entity, this.draftSaveHandler.bind(this)); + srv.before("READ", [target, target.drafts], this.setRepository.bind(this)) + srv.before("READ", [target, target.drafts], this.filterAttachments.bind(this)) + srv.before("SAVE", entity, this.renameHandler.bind(this)); + if (target.drafts) { + srv.before( + "PUT", + target.drafts, + this.draftSaveHandler.bind(this) + ); + } srv.after( ["DELETE", "UPDATE"], entity, diff --git a/lib/util/messageConsts.js b/lib/util/messageConsts.js index aed7367..bfa2e10 100644 --- a/lib/util/messageConsts.js +++ b/lib/util/messageConsts.js @@ -1,7 +1,5 @@ module.exports.duplicateDraftFileErr = (duplicateDraftFiles) => `The file(s) ${duplicateDraftFiles} have been added multiple times. Please rename and try again.`; -module.exports.emptyFileErr = (fileName) => - `Content of file ${fileName} is empty. Either it is corrupted or not uploaded properly.`; module.exports.virusFileErr = (virusFiles) => { const bulletPoints = virusFiles.map(file => `• ${file}`).join('\n'); return `The following files contain potential malware and cannot be uploaded:\n${bulletPoints}\n`; @@ -19,3 +17,6 @@ module.exports.otherFileErr = (otherFiles) => { return `${message}\n`; }; module.exports.versionedRepositoryErr = 'Attachments are not supported for a versioned repository.'; +module.exports.userNotAuthorisedError = 'You do not have the required permissions to upload attachments. Please contact your administrator for access.'; +module.exports.userDoesNotHaveScope = 'User does not have required scope'; +module.exports.errorMessage = 'An error occurred'; diff --git a/test/lib/handler/index.test.js b/test/lib/handler/index.test.js index 127b149..c509857 100644 --- a/test/lib/handler/index.test.js +++ b/test/lib/handler/index.test.js @@ -26,9 +26,11 @@ const { getFolderIdByPath, createFolder, deleteFolderWithAttachments, + getAttachment, renameAttachment, getRepositoryInfo } = require("../../../lib/handler/index"); +const { errorMessage } = require("../../../lib/util/messageConsts"); describe("handlers", () => { describe("ReadAttachment function", () => { @@ -381,7 +383,55 @@ describe("handlers", () => { }); }); - describe("renameAttachment function", () => { + describe('getAttachment', () => { + const uri = 'http://example.com/'; + const token = 'test-token'; + const objectId = 'test-object-id'; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch attachment successfully', async () => { + const mockResponse = { data: 'some data' }; + axios.get.mockResolvedValueOnce(mockResponse); + + const response = await getAttachment(uri, token, objectId); + + const expectedUrl =`${uri}browser/123/root?cmisselector=object&objectId=${objectId}&succinct=true`; + expect(axios.get).toHaveBeenCalledWith(expectedUrl, { headers: { Authorization: `Bearer ${token}` } }); + expect(response).toBe(mockResponse); + }); + + it('should return null and log status text on error', async () => { + const errorMessage = 'Not Found'; + const mockError = { + response: { + statusText: errorMessage, + }, + }; + axios.get.mockRejectedValueOnce(mockError); + console.log = jest.fn(); // Mock console.log + + const response = await getAttachment(uri, token, objectId); + + expect(console.log).toHaveBeenCalledWith(errorMessage); + expect(response).toBeNull(); + }); + + it('should return null and log a default error message when there is no status text', async () => { + const mockError = {}; + axios.get.mockRejectedValueOnce(mockError); + console.log = jest.fn(); // Mock console.log + + const response = await getAttachment(uri, token, objectId); + + expect(console.log).toHaveBeenCalledWith(errorMessage); + expect(response).toBeNull(); + }); + }); + + describe("renameAttachment", () => { beforeEach(() => { jest.clearAllMocks(); }); diff --git a/test/lib/sdm.test.js b/test/lib/sdm.test.js index a01a34a..e5e9f44 100644 --- a/test/lib/sdm.test.js +++ b/test/lib/sdm.test.js @@ -9,9 +9,11 @@ const { } = require("../../lib/util"); const { getDraftAttachments, + getDraftAttachmentsForUpID, getURLsToDeleteFromAttachments, getURLFromAttachments, getFolderIdForEntity, + updateAttachmentInDraft, setRepositoryId } = require("../../lib/persistence"); const { @@ -21,21 +23,29 @@ const { getFolderIdByPath, createFolder, deleteFolderWithAttachments, + getAttachment, renameAttachment, getRepositoryInfo } = require("../../lib/handler"); const { duplicateDraftFileErr, - emptyFileErr, + virusFileErr, + duplicateFileErr, + otherFileErr, + userNotAuthorisedError, + userDoesNotHaveScopeMessage, + versionedRepositoryErr } = require("../../lib/util/messageConsts"); jest.mock("@cap-js/attachments/lib/basic", () => class {}); jest.mock("../../lib/persistence", () => ({ getDraftAttachments: jest.fn(), + getDraftAttachmentsForUpID: jest.fn(), getDuplicateAttachments: jest.fn(), getURLsToDeleteFromAttachments: jest.fn(), getURLFromAttachments: jest.fn(), getFolderIdForEntity: jest.fn(), + updateAttachmentInDraft: jest.fn(), getExistingAttachments: jest.fn(), setRepositoryId: jest.fn() })); @@ -53,6 +63,7 @@ jest.mock("../../lib/handler", () => ({ getFolderIdByPath: jest.fn(), createFolder: jest.fn(), deleteFolderWithAttachments: jest.fn(), + getAttachment: jest.fn(), renameAttachment: jest.fn(), getRepositoryInfo: jest.fn() })); @@ -67,6 +78,72 @@ jest.mock("@sap/cds/lib", () => { jest.mock("node-cache"); describe("SDMAttachmentsService", () => { + describe("checkRepositoryType", () => { + let service; + let cache; + let cds; + + beforeEach(() => { + cds = require("@sap/cds/lib"); + cache = new NodeCache(); + NodeCache.mockImplementation(() => cache); + service = new SDMAttachmentsService(); + service.creds = { clientId: "client-id", clientSecret: "client-secret" }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should fetch repository info and check versioned status if not found in cache", async () => { + const mockReq = { reject: jest.fn() }; + cds.context = { + user: { + tokenInfo: { + getPayload: jest.fn().mockReturnValue({ ext_attr: { zdn: "test-subdomain" } }) + } + } + }; + + getConfigurations.mockReturnValue({ repositoryId: "repo123" }); + cache.get.mockReturnValue(undefined); + getClientCredentialsToken.mockResolvedValue("mock-token"); + getRepositoryInfo.mockResolvedValue({ data: "mock-repo-info" }); + isRepositoryVersioned.mockResolvedValue(false); + + await service.checkRepositoryType(mockReq); + + expect(getClientCredentialsToken).toHaveBeenCalledWith(service.creds); + expect(getRepositoryInfo).toHaveBeenCalledWith(service.creds, "mock-token"); + expect(isRepositoryVersioned).toHaveBeenCalledWith({ data: "mock-repo-info" }, "repo123"); + expect(mockReq.reject).not.toHaveBeenCalled(); + }); + + it("should reject the request if the repository is versioned", async () => { + const mockReq = { reject: jest.fn() }; + cds.context = { + user: { + tokenInfo: { + getPayload: jest.fn().mockReturnValue({ ext_attr: { zdn: "test-subdomain" } }) + } + } + }; + + getConfigurations.mockReturnValue({ repositoryId: "repo123" }); + cache.get.mockReturnValue(undefined); + getClientCredentialsToken.mockResolvedValue("mock-token"); + getRepositoryInfo.mockResolvedValue({ data: "mock-repo-info" }); + isRepositoryVersioned.mockResolvedValue(true); + + await service.checkRepositoryType(mockReq); + + expect(getClientCredentialsToken).toHaveBeenCalledWith(service.creds); + expect(getRepositoryInfo).toHaveBeenCalledWith(service.creds, "mock-token"); + expect(isRepositoryVersioned).toHaveBeenCalledWith({ data: "mock-repo-info" }, "repo123"); + expect(mockReq.reject).toHaveBeenCalledWith(400, versionedRepositoryErr); + }); + }); + describe("Test get method", () => { let service; let repoInfo @@ -103,8 +180,8 @@ describe("SDMAttachmentsService", () => { }, }, }; - cds = require("@sap/cds/lib"); -cds.context = { + let cds = require("@sap/cds/lib"); + cds.context = { user: { tokenInfo: { getPayload: jest.fn(() => ({ @@ -114,7 +191,7 @@ cds.context = { })), }, }, - }; + }; const attachments = ["attachment1", "attachment2"]; const keys = ["key1", "key2"]; const response = { url: "mockUrl" }; @@ -145,7 +222,7 @@ cds.context = { }, }, }; - cds = require("@sap/cds/lib"); + let cds = require("@sap/cds/lib"); cds.context = { user: { tokenInfo: { @@ -185,69 +262,6 @@ cds.context = { ); }); - it("should throw error if repositoryId is versioned", async () => { - const req = { - user: { - tokenInfo: { - getTokenValue: jest.fn().mockReturnValue("tokenValue"), - }, - }, - reject: jest.fn(), - }; - cds = require("@sap/cds/lib"); - cds.context = { - user: { - tokenInfo: { - getPayload: jest.fn(() => ({ - ext_attr: { - zdn: 'subdomain' // simulate the subdomain extraction - } - })), - }, - }, - }; - const attachments = ["attachment1", "attachment2"]; - const keys = ["key1", "key2"]; - isRepositoryVersioned.mockResolvedValue(true); - await service.get(attachments, keys, req); - expect(req.reject).toHaveBeenCalledWith( - 400, - "Attachments are not supported for a versioned repository." - ); - }) - - it("should throw error if cache returns repositoryId is versioned", async () => { - NodeCache.prototype.get.mockImplementation(() => "versioned"); - const req = { - user: { - tokenInfo: { - getTokenValue: jest.fn().mockReturnValue("tokenValue"), - }, - }, - reject: jest.fn(), - }; - cds = require("@sap/cds/lib"); - cds.context = { - user: { - tokenInfo: { - getPayload: jest.fn(() => ({ - ext_attr: { - zdn: 'subdomain' // simulate the subdomain extraction - } - })), - }, - }, - }; - const attachments = ["attachment1", "attachment2"]; - const keys = ["key1", "key2"]; - isRepositoryVersioned.mockResolvedValue(true); - await service.get(attachments, keys, req); - expect(req.reject).toHaveBeenCalledWith( - 400, - "Attachments are not supported for a versioned repository." - ); - }) - it("should interact with DB, fetch access token and readAttachment with correct parameters when cache returns non-versioned repo type", async () => { NodeCache.prototype.get.mockImplementation(() => "non-versioned"); const req = { @@ -257,8 +271,8 @@ cds.context = { }, }, }; - cds = require("@sap/cds/lib"); -cds.context = { + let cds = require("@sap/cds/lib"); + cds.context = { user: { tokenInfo: { getPayload: jest.fn(() => ({ @@ -268,7 +282,7 @@ cds.context = { })), }, }, - }; + }; const attachments = ["attachment1", "attachment2"]; const keys = ["key1", "key2"]; const response = { url: "mockUrl" }; @@ -291,192 +305,303 @@ cds.context = { ); }); }); - - describe("draftSaveHandler", () => { + + describe('renameHandler', () => { let service; - let mockReq; - let cds; - let repoInfo; + let req; + let token; + beforeEach(() => { - NodeCache.prototype.get.mockClear(); jest.clearAllMocks(); cds = require("@sap/cds/lib"); service = new SDMAttachmentsService(); - service.creds = { uaa: "mocked uaa" }; - repoInfo = { - data: { - "123": { - capabilities: { - "capabilityContentStreamUpdatability": "pwconly" - } - } - } - } - mockReq = { + getConfigurations.mockReturnValue({ repositoryId: 'repo123' }); + service.creds = { + uri: 'sampleUri' + }; + req = { query: { target: { - name: "testName", - }, + name: 'sampleTarget' + } }, user: { tokenInfo: { - getTokenValue: jest.fn().mockReturnValue("mocked_token"), - }, + getTokenValue: jest.fn().mockReturnValue('sampleTokenValue') + } }, - reject: jest.fn(), - info: jest.fn(), warn: jest.fn() }; - cds = require("@sap/cds/lib"); - cds.model.definitions[mockReq.query.target.name + ".attachments"] = { - keys: { - up_: { - keys: [{ ref: ["attachment"] }], - }, - }, - }; - cds.context = { - user: { - tokenInfo: { - getPayload: jest.fn(() => ({ - ext_attr: { - zdn: 'subdomain' // simulate the subdomain extraction - } - })), - }, - }, - }; - NodeCache.prototype.get.mockImplementation(() => undefined); - getConfigurations.mockReturnValue({ - repositoryId: 'mockRepositoryId', + token = 'sampleAccessToken'; + }); + + it('should rename modified attachments', async () => { + service.checkRepositoryType = jest.fn().mockResolvedValue(); + service.isFileNameDuplicateInDrafts = jest.fn().mockResolvedValue(); + service.getAttachementDataInSDM = jest.fn((uri, token, objectId) => { + if (objectId === 'url2') { + return { filename: 'sampleFileName', folderId: 'sampleFolderId' }; + } + return { filename: 'prevFile1', folderId: 'sampleFolderId' }; }); - getRepositoryInfo.mockResolvedValueOnce(repoInfo); - isRepositoryVersioned.mockResolvedValueOnce(false); + service.rename = jest.fn().mockResolvedValue('error occurred'); + + fetchAccessToken.mockResolvedValue(token); + getDraftAttachments.mockResolvedValue([ + { ID: 1, HasActiveEntity: true, filename: 'file1', url: 'url1' }, + { ID: 2, HasActiveEntity: false, filename: 'fileDraft', url: 'url2' } + ]); + checkAttachmentsToRename.mockResolvedValue([{ ID: 1, url: 'url1', name: 'file1', prevname: 'prevFile1', folderId: 'sampleFolderId' }]); + + await service.renameHandler(req); + + expect(service.checkRepositoryType).toHaveBeenCalledWith(req); + expect(service.isFileNameDuplicateInDrafts).toHaveBeenCalled(); + expect(fetchAccessToken).toHaveBeenCalledWith(service.creds, 'sampleTokenValue'); + expect(getDraftAttachments).toHaveBeenCalledWith(cds.model.definitions['sampleTarget.attachments'], req, undefined); + expect(service.getAttachementDataInSDM).toHaveBeenCalledWith(service.creds.uri, token, 'url2'); + expect(checkAttachmentsToRename).toHaveBeenCalled(); + expect(service.rename).toHaveBeenCalledWith( + [{ ID: 1, url: 'url1', name: 'file1', prevname: 'prevFile1', folderId: 'sampleFolderId' }, + { ID: 2, url: 'url2', name: 'fileDraft', prevname: 'sampleFileName', folderId: 'sampleFolderId' }], + token, + req + ); + expect(req.warn).toHaveBeenCalledWith(500, 'error occurred'); }); - - 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); + + it('should not rename if no attachments are modified', async () => { + service.checkRepositoryType = jest.fn().mockResolvedValue(); + service.isFileNameDuplicateInDrafts = jest.fn().mockResolvedValue(); + service.getAttachementDataInSDM = jest.fn().mockResolvedValue({ filename: 'fileDraft', folderId: 'folderId' }); + service.rename = jest.fn(); + + fetchAccessToken.mockResolvedValue(token); + getDraftAttachments.mockResolvedValue([]); + checkAttachmentsToRename.mockResolvedValue([]); + + await service.renameHandler(req); + + expect(service.checkRepositoryType).toHaveBeenCalledWith(req); + expect(service.isFileNameDuplicateInDrafts).not.toHaveBeenCalled(); + expect(fetchAccessToken).not.toHaveBeenCalled(); + expect(getDraftAttachments).toHaveBeenCalledWith(cds.model.definitions['sampleTarget.attachments'], req, undefined); + expect(service.getAttachementDataInSDM).not.toHaveBeenCalled(); + expect(checkAttachmentsToRename).not.toHaveBeenCalled(); + expect(service.rename).not.toHaveBeenCalled(); + expect(req.warn).not.toHaveBeenCalled(); }); - it("should not call onCreate if no draft attachments are available", async () => { - getDraftAttachments.mockResolvedValueOnce([]); - const createSpy = jest.spyOn(service, "create"); - await service.draftSaveHandler(mockReq); - - expect(createSpy).not.toBeCalled(); + it('should not modify attachments if filenameInDraft equals filenameInSDM', async () => { + service.checkRepositoryType = jest.fn().mockResolvedValue(); + service.isFileNameDuplicateInDrafts = jest.fn().mockResolvedValue(); + service.getAttachementDataInSDM = jest.fn().mockResolvedValue({ filename: 'fileDraft', folderId: 'sampleFolderId' }); + service.rename = jest.fn().mockResolvedValue(''); + + fetchAccessToken.mockResolvedValue(token); + getDraftAttachments.mockResolvedValue([ + { ID: 1, HasActiveEntity: true, filename: 'file1', url: 'url1' }, + { ID: 2, HasActiveEntity: false, filename: 'fileDraft', url: 'url2' } + ]); + checkAttachmentsToRename.mockResolvedValue([]); + + await service.renameHandler(req); + + expect(service.checkRepositoryType).toHaveBeenCalledWith(req); + expect(service.isFileNameDuplicateInDrafts).toHaveBeenCalled(); + expect(fetchAccessToken).toHaveBeenCalledWith(service.creds, 'sampleTokenValue'); + expect(getDraftAttachments).toHaveBeenCalledWith(cds.model.definitions['sampleTarget.attachments'], req, undefined); + expect(service.getAttachementDataInSDM).toHaveBeenCalledWith(service.creds.uri, token, 'url2'); + expect(checkAttachmentsToRename).toHaveBeenCalled(); + expect(req.warn).not.toHaveBeenCalled(); }); - it("should handle successful create without any issue", async () => { - service.create = jest.fn().mockResolvedValueOnce([]); - const createSpy = jest.spyOn(service, "create"); - getDraftAttachments.mockResolvedValueOnce([{'HasActiveEntity':false}]); - checkAttachmentsToRename.mockResolvedValueOnce([]); + it('should avoid renaming if there are no modified attachments', async () => { + service.checkRepositoryType = jest.fn().mockResolvedValue(); + service.isFileNameDuplicateInDrafts = jest.fn().mockResolvedValue(); + service.getAttachementDataInSDM = jest.fn().mockResolvedValue({ filename: 'fileDraft', folderId: 'sampleFolderId' }); + service.rename = jest.fn().mockResolvedValue(''); + + fetchAccessToken.mockResolvedValue(token); + getDraftAttachments.mockResolvedValue([ + { ID: 1, HasActiveEntity: true, filename: 'file1', url: 'url1' } + ]); + checkAttachmentsToRename.mockResolvedValue([]); + + await service.renameHandler(req); + + expect(service.checkRepositoryType).toHaveBeenCalledWith(req); + expect(service.isFileNameDuplicateInDrafts).toHaveBeenCalled(); + expect(fetchAccessToken).toHaveBeenCalledWith(service.creds, 'sampleTokenValue'); + expect(checkAttachmentsToRename).toHaveBeenCalled(); + expect(service.rename).not.toHaveBeenCalled(); + expect(req.warn).not.toHaveBeenCalled(); + }); + }); - await service.draftSaveHandler(mockReq); + describe('getAttachementDataInSDM', () => { + let service; + const uri = 'someUri'; + const token = 'someToken'; + const objectId = 'someObjectId'; - expect(createSpy).toBeCalled(); - expect(mockReq.warn).not.toBeCalled(); + beforeEach(() => { + jest.clearAllMocks(); + service = new SDMAttachmentsService(); }); - - it("should handle successful onRename without any issue", async () => { - service.rename = jest.fn().mockResolvedValueOnce([]); - const renameSpy = jest.spyOn(service, "rename"); - getDraftAttachments.mockResolvedValueOnce([ - { - 'ID': 'id1', - 'filename': 'nameprev', - 'HasActiveEntity' : true + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return formatted attachment data correctly', async () => { + // Arrange + const mockResponse = { + data: { + succinctProperties: { + 'cmis:name': 'testFileName.docx', + 'sap:parentIds': ['parentId123'], + }, }, - { - 'ID': 'id2', - 'filename': 'samename', - 'HasActiveEntity' : true - }]); - checkAttachmentsToRename.mockResolvedValueOnce([{}]); - - await service.draftSaveHandler(mockReq); - - expect(renameSpy).toBeCalled(); - expect(mockReq.warn).not.toBeCalled(); + }; + getAttachment.mockResolvedValue(mockResponse); + + // Act + const result = await service.getAttachementDataInSDM(uri, token, objectId); + + // Assert + expect(result).toEqual({ + filename: 'testFileName.docx', + folderId: 'parentId123', + }); }); - - it("should not call rename if no draft attachments are available", async () => { - getDraftAttachments.mockResolvedValueOnce([]); - const renameSpy = jest.spyOn(service, "rename"); - await service.draftSaveHandler(mockReq); - - expect(renameSpy).not.toBeCalled(); + + it('should throw an error if getAttachment throws an error', async () => { + // Arrange + const mockError = new Error('Some error'); + getAttachment.mockRejectedValue(mockError); + + // Act & Assert + await expect(service.getAttachementDataInSDM(uri, token, objectId)).rejects.toThrow('Some error'); }); - - it("should call only onCreate if only new attachments are available in draft", async () => { - service.create = jest.fn().mockResolvedValueOnce([]); - const createSpy = jest.spyOn(service, "create"); - const renameSpy = jest.spyOn(service, "rename"); - getDraftAttachments.mockResolvedValueOnce([{'HasActiveEntity' : false}]); - checkAttachmentsToRename.mockResolvedValueOnce([]); - - await service.draftSaveHandler(mockReq); - - expect(createSpy).toBeCalled(); - expect(renameSpy).not.toBeCalled(); + + it('should return undefined folderId if parentIds array is empty', async () => { + // Arrange + const mockResponse = { + data: { + succinctProperties: { + 'cmis:name': 'testFileName.docx', + 'sap:parentIds': [], + }, + }, + }; + getAttachment.mockResolvedValue(mockResponse); + + // Act + const result = await service.getAttachementDataInSDM(uri, token, objectId); + + // Assert + expect(result).toEqual({ + filename: 'testFileName.docx', + folderId: undefined, + }); }); + }); - it("should call only onRename if only modified attachments are available in draft", async () => { - service.rename = jest.fn().mockResolvedValueOnce([]); - const createSpy = jest.spyOn(service, "create"); - const renameSpy = jest.spyOn(service, "rename"); - getDraftAttachments.mockResolvedValueOnce([ - { - 'ID': 'id', - 'filename': 'nameprev', - 'HasActiveEntity' : true - }]); - checkAttachmentsToRename.mockResolvedValueOnce([{}]); + describe('draftSaveHandler', () => { + let service; + beforeEach(() => { + jest.clearAllMocks(); + service = new SDMAttachmentsService(); + getConfigurations.mockReturnValue({ repositoryId: 'repo123' }); + service.checkRepositoryType = jest.fn(); + service.isFileNameDuplicateInDrafts = jest.fn(); + service.create = jest.fn(); + service.creds = {}; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should skip when req.data.content is not provided', async () => { + const req = { data: {} }; + await service.draftSaveHandler(req); + expect(service.checkRepositoryType).not.toHaveBeenCalled(); + }); + + test('should handle drafts when attachment values are found', async () => { + const draftAttachments = []; + const req = { data: { content: 'some content', ID: '12345' }, target: draftAttachments, user: { tokenInfo: { getTokenValue: jest.fn().mockReturnValue('mockTokenValue') } } }; + const token = 'token123'; + const attachment_val = [ + { HasActiveEntity: false, ID: '12345' }, + { HasActiveEntity: true, ID: '67890' }, + ]; + getDraftAttachmentsForUpID.mockResolvedValue(attachment_val); + fetchAccessToken.mockResolvedValue(token); + + await service.draftSaveHandler(req); - await service.draftSaveHandler(mockReq); - - expect(createSpy).not.toBeCalled(); - expect(renameSpy).toBeCalled(); + expect(service.isFileNameDuplicateInDrafts).toHaveBeenCalledWith(attachment_val, req); + expect(service.create).toHaveBeenCalledWith([{ ...attachment_val[0], content: 'some content' }], draftAttachments, req, token); + expect(req.data.content).toBeNull(); + }); + + test('should not create attachment if no matching inactive entities are found', async () => { + const draftAttachments = []; + const req = { data: { content: 'some content', ID: '12345' }, target: draftAttachments, user: { tokenInfo: { getTokenValue: jest.fn().mockReturnValue('mockTokenValue') } } }; + const token = 'token123'; + const attachment_val = [{ HasActiveEntity: true, ID: '12345' }]; + + getDraftAttachmentsForUpID.mockResolvedValue(attachment_val); + fetchAccessToken.mockResolvedValue(token); + + await service.draftSaveHandler(req); + + expect(service.create).not.toHaveBeenCalled(); + expect(req.data.content).toBeNull(); }); - it("should call both onRename and onCreate if both modified and new attachments are available in draft", async () => { - service.rename = jest.fn().mockResolvedValueOnce([]); - service.create = jest.fn().mockResolvedValueOnce([]); - const createSpy = jest.spyOn(service, "create"); - const renameSpy = jest.spyOn(service, "rename"); - getDraftAttachments.mockResolvedValueOnce([ - { - 'ID': 'id', - 'filename': 'nameprev', - 'HasActiveEntity' : true - }, - { - 'HasActiveEntity' : false - }]); - checkAttachmentsToRename.mockResolvedValueOnce([{}]); - - await service.draftSaveHandler(mockReq); + test('should skip when no attachments are found', async () => { + const draftAttachments = []; + const req = { data: { content: 'some content', ID: '12345' }, target: draftAttachments, user: { tokenInfo: { getTokenValue: jest.fn().mockReturnValue('mockTokenValue') } } }; + const attachment_val = []; + + getDraftAttachmentsForUpID.mockResolvedValue(attachment_val); + + await service.draftSaveHandler(req); + + expect(service.isFileNameDuplicateInDrafts).not.toHaveBeenCalled(); + expect(service.create).not.toHaveBeenCalled(); + expect(req.data.content).toBeNull(); + }); - expect(createSpy).toBeCalled(); - expect(renameSpy).toBeCalled(); + test('should skip processing when req.data.content is null after initial check', async () => { + const draftAttachments = []; + const req = { data: { content: null, ID: '12345' }, target: draftAttachments, user: { tokenInfo: { getTokenValue: jest.fn().mockReturnValue('mockTokenValue') } } }; + const attachment_val = [ + { HasActiveEntity: false, ID: '12345' }, + { HasActiveEntity: true, ID: '67890' }, + ]; + getDraftAttachmentsForUpID.mockResolvedValue(attachment_val); + + req.data.content = null; // simulating content being reset to null after initial check + + await service.draftSaveHandler(req); + + expect(service.isFileNameDuplicateInDrafts).not.toHaveBeenCalled(); + expect(service.create).not.toHaveBeenCalled(); }); }); - describe("Test filterAttachments", () => { + describe("filterAttachments", () => { let service; - let mockReq; + let mockedReq; beforeEach(() => { jest.clearAllMocks(); service = new SDMAttachmentsService(); - getConfigurations.mockReturnValue({ - repositoryId: 'mockRepositoryId', - }); - mockReq = { + mockedReq = { query: { SELECT: {}, }, @@ -485,13 +610,16 @@ cds.context = { getTokenValue: jest.fn().mockReturnValue("mocked_token"), } } - } + }; + getConfigurations.mockReturnValue({ + repositoryId: 'mockRepositoryId', + }); }); it("should add a condition to filter attachments by repositoryId when where clause is empty", async() => { - mockReq.query.SELECT.where = []; - await service.filterAttachments(mockReq); - expect(mockReq.query.SELECT.where).toEqual([ + mockedReq.query.SELECT.where = []; + await service.filterAttachments(mockedReq); + expect(mockedReq.query.SELECT.where).toEqual([ { ref: ['repositoryId'] }, '=', { val: "mockRepositoryId" } @@ -499,9 +627,9 @@ cds.context = { }); it("should add a condition to filter attachments by repositoryId when where clause already exists", async() => { - mockReq.query.SELECT.where = [{ ref: ['someField'] }, '=', { val: 'someValue' }]; - await service.filterAttachments(mockReq); - expect(mockReq.query.SELECT.where).toEqual([ + mockedReq.query.SELECT.where = [{ ref: ['someField'] }, '=', { val: 'someValue' }]; + await service.filterAttachments(mockedReq); + expect(mockedReq.query.SELECT.where).toEqual([ { ref: ['someField'] }, '=', { val: 'someValue' }, @@ -513,8 +641,8 @@ cds.context = { }); it("should add a condition to filter attachments by repositoryId when where clause doesn't exist", async() => { - await service.filterAttachments(mockReq); - expect(mockReq.query.SELECT.where).toEqual([ + await service.filterAttachments(mockedReq); + expect(mockedReq.query.SELECT.where).toEqual([ { ref: ['repositoryId'] }, '=', { val: "mockRepositoryId" } @@ -522,13 +650,12 @@ cds.context = { }); }); - describe("Test setRepository", () => { + describe("setRepository", () => { let service; beforeEach(() => { jest.clearAllMocks(); service = new SDMAttachmentsService(); - cds = require("@sap/cds/lib"); getConfigurations.mockReturnValue({ repositoryId: 'mockRepositoryId', @@ -789,7 +916,6 @@ cds.context = { let service; beforeEach(() => { jest.clearAllMocks(); - cds = require("@sap/cds/lib"); service = new SDMAttachmentsService(); }); it("should delete attachments if req.attachmentsToDelete has records to delete", async () => { @@ -956,44 +1082,6 @@ cds.context = { expect(onCreateSpy).toBeCalled(); expect(getParentIdSpy).toBeCalled(); - expect(mockReq.warn).not.toBeCalled(); - }) - - it("should handle failure in onCreate", async () => { - const attachment_val_create = [{}]; - const token = "token"; - const attachments = []; - - service.getParentId = jest.fn().mockResolvedValueOnce("parentId"); - service.onCreate = jest.fn().mockResolvedValue([{typeOfError:'duplicate',name:'sample.pdf'},{typeOfError:'virus',name:'virus.pdf'},{typeOfError:'other',message:'Child invalid.pdf with Id abc is not a valid file type'}]); - - let response = await service.create( - attachment_val_create, - attachments, - mockReq, - token - ); - - expect(response).toBe("The following files contain potential malware and cannot be uploaded:\n• virus.pdf\nThe following files could not be uploaded as they already exist:\n• sample.pdf\nChild invalid.pdf with Id abc is not a valid file type\n"); - }) - - it("should handle failure in onCreate after failure in rename", async () => { - const attachment_val_create = [{}]; - const token = "token"; - const attachments = []; - - service.getParentId = jest.fn().mockResolvedValueOnce("parentId"); - service.onCreate = jest.fn().mockResolvedValue([{typeOfError:'duplicate',name:'sample.pdf'},{typeOfError:'virus',name:'virus.pdf'},{typeOfError:'other',message:'Child invalid.pdf with Id abc is not a valid file type'}]); - - let response = await service.create( - attachment_val_create, - attachments, - mockReq, - token, - "rename_error\n" - ); - - expect(response).toBe("rename_error\nThe following files contain potential malware and cannot be uploaded:\n• virus.pdf\nThe following files could not be uploaded as they already exist:\n• sample.pdf\nChild invalid.pdf with Id abc is not a valid file type\n"); }) }); @@ -1078,132 +1166,71 @@ cds.context = { }) }); - describe("onCreate", () => { - let service; + describe('onCreate', () => { + let data, credentials, token, req, parentId, service; + beforeEach(() => { jest.clearAllMocks(); service = new SDMAttachmentsService(); - }); - - it("should return empty array if no attachments fail", async () => { - const data = [{ ID: 1 }]; - const credentials = {}; - const token = "token"; - const attachments = []; - const req = { - data: { attachments: [...data] }, - user: { - tokenInfo: { - getTokenValue: jest.fn().mockReturnValue("tokenValue"), - }, - }, + getConfigurations.mockReturnValue({ repositoryId: 'repo123' }); + data = [{ filename: 'file1' }]; + credentials = { user: 'user', pass: 'pass' }; + token = 'token'; + req = { + reject: jest.fn(), }; - + parentId = 'parent123'; + }); + + it('should successfully create attachments and update draft', async () => { createAttachment .mockResolvedValueOnce({ status: 201, - data: { succinctProperties: { "cmis:objectId": "url" } }, - }) - - const result = await service.onCreate( - data, - credentials, - token, - attachments, - req, - createAttachment - ); - expect(result).toEqual([]); + data: { succinctProperties: { 'cmis:objectId': 'url1' } }, + }); + updateAttachmentInDraft.mockResolvedValue(true); + + await service.onCreate(data, credentials, token, req, parentId); + + expect(createAttachment).toHaveBeenCalledTimes(1); + expect(updateAttachmentInDraft).toHaveBeenCalledTimes(1); + expect(req.reject).not.toHaveBeenCalled(); }); - - 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], - user: { - tokenInfo: { - getTokenValue: jest.fn().mockReturnValue("tokenValue"), - }, - }, - }, - }; - const token = "mocked_token"; - const credentials = "mocked_credentials"; - const attachments = "mocked_attachments"; - const parentId = "mocked_parentId"; - - createAttachment.mockResolvedValueOnce({ - status: 201, - data: { succinctProperties: { "cmis:objectId": "some_object_id" } }, + + it('should reject when a virus is found in the file', async () => { + createAttachment + .mockResolvedValueOnce({ + status: 500, + response: { data: { message: "Malware Service Exception: Virus found in the file!" } } }); - - 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, - repositoryId: "mockRepositoryId", - url: "some_object_id", + + await service.onCreate(data, credentials, token, req, parentId); + + expect(req.reject).toHaveBeenCalledWith(virusFileErr(['file1'])); + }); + + it('should reject when there is a name constraint violation', async () => { + createAttachment + .mockResolvedValueOnce({ + status: 500, + response: { data: { exception: "nameConstraintViolation" } } }); + + await service.onCreate(data, credentials, token, req, parentId); + + expect(req.reject).toHaveBeenCalledWith(duplicateFileErr(['file1'])); }); - - it("should return failed request messages if some attachments fail", async () => { - const data = [{ ID: 1 }, { ID: 2 }, { ID: 3}, { ID: 4}]; - const credentials = {}; - const token = "token"; - const attachments = []; - const req = { - data: { attachments: [...data] }, - user: { - tokenInfo: { - getTokenValue: jest.fn().mockReturnValue("tokenValue"), - }, - }, - }; - + + it('should reject when another error occurs', async () => { createAttachment - .mockResolvedValueOnce({ - status: 201, - data: { succinctProperties: { "cmis:objectId": "url" } }, - }) - .mockResolvedValueOnce({ - status: 400, - response: { data: { exception: "nameConstraintViolation" } }, - }) - .mockResolvedValueOnce({ - status: 500, - response: { data: { message: "Malware Service Exception: Virus found in the file!" } } - }) - .mockResolvedValueOnce({ - status: 500, - response: { data: { message: "Invalid file type" } } - }); - - const result = await service.onCreate( - data, - credentials, - token, - attachments, - req, - createAttachment - ); - expect(result).toEqual([{ typeOfError:'duplicate', "name": undefined }, { typeOfError:'virus', "name": undefined }, { typeOfError:'other', message: "Invalid file type" }]); - expect(req.data.attachments).toHaveLength(1); + .mockResolvedValueOnce({ + status: 500, + response: { data: { exception: "some other error" } } + }); + + await service.onCreate(data, credentials, token, req, parentId); + + expect(req.reject).toHaveBeenCalledWith(otherFileErr(['file1'])); }); }); @@ -1299,6 +1326,7 @@ cds.context = { beforeEach(() => { jest.clearAllMocks(); cds = require("@sap/cds/lib"); + getConfigurations.mockReturnValue({ repositoryId: 'repo123' }); service = new SDMAttachmentsService(); service.creds = { uaa: "mocked uaa" }; mockReq = { @@ -1366,19 +1394,45 @@ cds.context = { ); }); - it("Success in getParentId", async () => { - let attachments = cds.model.definitions[mockReq.query.target.name + ".attachments"] - let token = "mocked_token" - getFolderIdForEntity.mockResolvedValueOnce(["folderId"]); + it("getParentId should reject with 403 if createFolder response status is 403 and message matches userDoesNotHaveScopeMessage", async () => { + let attachments = cds.model.definitions[mockReq.query.target.name + ".attachments"]; + let token = "mocked_token"; + getFolderIdForEntity.mockResolvedValueOnce([]); + getFolderIdByPath.mockResolvedValueOnce(null); + createFolder.mockResolvedValueOnce({ + status: 403, + response: { + data: userDoesNotHaveScopeMessage + } + }); - await service.getParentId(attachments,mockReq,token) - - expect(getFolderIdForEntity).toHaveBeenCalledWith( - cds.model.definitions[mockReq.query.target.name + ".attachments"], - mockReq, - "mockRepositoryId" - ); - }); + try { + await service.getParentId(attachments, mockReq, token); + } catch (err) { + console.error("Error in getParentId:", err); + } + + expect(mockReq.reject).toHaveBeenCalledWith(403, userNotAuthorisedError); + }); + + it("getParentId should return parentId if folderId is not null in folderIds", async () => { + let attachments = cds.model.definitions[mockReq.query.target.name + ".attachments"]; + let token = "mocked_token"; + + const folderIds = [ + { folderId: null }, + { folderId: "mock_folder_id_1" }, + { folderId: "mock_folder_id_2" } + ]; + + getFolderIdForEntity.mockResolvedValueOnce(folderIds); + + const parentId = await service.getParentId(attachments, mockReq, token); + + expect(parentId).toEqual("mock_folder_id_1"); + expect(getFolderIdByPath).not.toHaveBeenCalled(); + expect(createFolder).not.toHaveBeenCalled(); + }); }); describe("isFileNameDuplicateInDrafts", () => { @@ -1387,7 +1441,6 @@ cds.context = { beforeEach(() => { jest.clearAllMocks(); - cds = require("@sap/cds/lib"); service = new SDMAttachmentsService(); mockReq = { query: { @@ -1477,6 +1530,20 @@ cds.context = { }); }); + describe('getStatus', () => { + let service; + + beforeEach(() => { + jest.clearAllMocks(); + service = new SDMAttachmentsService(); + }); + + it('should return the status as "Clean"', async () => { + const status = await service.getStatus(); + expect(status).toBe("Clean"); + }); + }); + describe("registerUpdateHandlers", () => { let mockSrv; let service; @@ -1516,5 +1583,24 @@ cds.context = { expect.any(Function) ); }); + it("should call srv.before for PUT with correct target.drafts and callback", () => { + const target = { drafts: "drafts" }; + service.registerUpdateHandlers(mockSrv, "entity", target); + expect(mockSrv.before).toHaveBeenCalledWith( + "PUT", + target.drafts, + expect.any(Function) + ); + }); + + it("should not call srv.before for PUT when target.drafts is not defined", () => { + const target = {}; + service.registerUpdateHandlers(mockSrv, "entity", target); + expect(mockSrv.before).not.toHaveBeenCalledWith( + "PUT", + undefined, + expect.any(Function) + ); + }); }); }); \ No newline at end of file