diff --git a/lib/sdm.js b/lib/sdm.js index 7506212..0e26109 100644 --- a/lib/sdm.js +++ b/lib/sdm.js @@ -27,9 +27,9 @@ module.exports = class SDMAttachmentsService extends ( return this.creds; } - async get(attachments, keys) { + async get(attachments, keys, req) { const response = await getURLFromAttachments(keys, attachments); - const token = await fetchAccessToken(this.creds); + const token = await fetchAccessToken(this.creds, req.user.tokenInfo.getTokenValue()); try { const Key = response?.url; const content = await readAttachment(Key, token, this.creds); @@ -50,7 +50,7 @@ module.exports = class SDMAttachmentsService extends ( if (duplicateDraftFilesErrMsg != "") { req.reject(409, duplicateDraftFileErr(duplicateDraftFilesErrMsg)); } - const token = await fetchAccessToken(this.creds); + const token = await fetchAccessToken(this.creds, req.user.tokenInfo.getTokenValue()); console.log("Token: ", token); const folderIds = await getFolderIdForEntity(attachments, req); let parentId = ""; @@ -146,7 +146,7 @@ module.exports = class SDMAttachmentsService extends ( cds.model.definitions[req.query.target.name + ".attachments"]; if (req?.attachmentsToDelete?.length > 0) { - const token = await fetchAccessToken(this.creds); + const token = await fetchAccessToken(this.creds, req.user.tokenInfo.getTokenValue()); if (req?.parentId) { await deleteFolderWithAttachments(this.creds, token, req.parentId); } else { @@ -264,4 +264,4 @@ module.exports = class SDMAttachmentsService extends ( ); return; } -}; +}; \ No newline at end of file diff --git a/lib/util/index.js b/lib/util/index.js index 0ed908a..76fa235 100644 --- a/lib/util/index.js +++ b/lib/util/index.js @@ -3,31 +3,52 @@ const cds = require("@sap/cds"); const requests = xssec.requests; const NodeCache = require("node-cache"); const cache = new NodeCache(); - -function fetchAccessToken(credentials) { - const access_token = cache.get("SDM_ACCESS_TOKEN"); // to check if token exists +async function fetchAccessToken(credentials, jwt) { + let decoded_token_jwt = decodeAccessToken(jwt); + let access_token = cache.get(decoded_token_jwt.email); // to check if token exists if (access_token === undefined) { - return new Promise(function (resolve, reject) { - requests.requestClientCredentialsToken( - null, - credentials.uaa, - null, - (error, response) => { - if (error) { - console.error( - `Response error while fetching access token ${response.statusCode}` - ); - reject(err); - } else { - cache.set("SDM_ACCESS_TOKEN", response, 11); //expires after 11 hours - resolve(response); - } - } - ); - }); + access_token = await generateSDMBearerToken(credentials, jwt); + let user = decodeAccessToken(access_token).email; + cache.set(user, access_token, 11 * 3600); //expires after 11 hours } else { - return access_token; + let decoded_token = decodeAccessToken(access_token); + if (isTokenExpired(decoded_token.exp)) { + access_token = generateSDMBearerToken(credentials, jwt); + cache.del(decoded_token.email); + cache.set(decoded_token.email, access_token, 11 * 3600); //expires after 11 hours + } + } + return access_token; +} +async function generateSDMBearerToken(credentials, jwt) { + return new Promise(function (resolve, reject) { + requests.requestUserToken( + jwt, + credentials.uaa, + null, null, null, null, (error, response) => { + if (error) { + console.error( + `Response error while fetching access token ${response.statusCode}` + ); + reject(err); + } else { + resolve(response); + return response; + } + } + ); + }); +} +function isTokenExpired(exp) { + var expiry = new Date(exp * 1000); + var now = new Date(); + return now > expiry; +} +function decodeAccessToken(jwtEncoded) { + const jwtBase64Encoded = jwtEncoded.split('.')[1]; + const jwtDecodedAsString = Buffer.from(jwtBase64Encoded, 'base64').toString('ascii'); + return JSON.parse(jwtDecodedAsString); } function getConfigurations() { diff --git a/test/lib/sdm.test.js b/test/lib/sdm.test.js index 8333eac..173a365 100644 --- a/test/lib/sdm.test.js +++ b/test/lib/sdm.test.js @@ -57,50 +57,68 @@ describe("SDMAttachmentsService", () => { }); it("should interact with DB, fetch access token and readAttachment with correct parameters", async () => { + const req = { + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, + }, + }; + const attachments = ["attachment1", "attachment2"]; const keys = ["key1", "key2"]; - const token = "dummy_token"; + const token = "mocked_token"; const response = { url: "mockUrl" }; - - fetchAccessToken.mockResolvedValueOnce(token); + + // set req in service instance + getURLFromAttachments.mockResolvedValueOnce(response); - - await service.get(attachments, keys); - - expect(getURLFromAttachments).toHaveBeenCalledWith(keys, attachments); - expect(fetchAccessToken).toHaveBeenCalledWith(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" }; - fetchAccessToken.mockResolvedValueOnce(token); - getURLFromAttachments.mockResolvedValueOnce(response); - // Make readAttachment to throw error - readAttachment.mockImplementationOnce(() => { - throw new Error("Error reading attachment"); - }); - - await expect(service.get(attachments, keys)).rejects.toThrow( - "Error reading attachment" - ); - + readAttachment.mockResolvedValueOnce('dummy_content'); + + await service.get(attachments, keys,req); // call get method + expect(getURLFromAttachments).toHaveBeenCalledWith(keys, attachments); - expect(fetchAccessToken).toHaveBeenCalledWith(service.creds); + expect(fetchAccessToken).toHaveBeenCalledWith(service.creds, 'tokenValue'); expect(readAttachment).toHaveBeenCalledWith( "mockUrl", token, service.creds ); }); + + it("should throw error if readAttachment fails", async () => { + const req = { + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, + }, + }; + const attachments = ["attachment1", "attachment2"]; + const keys = ["key1", "key2"]; + const token = "mocked_token"; + const response = { url: "mockUrl" }; + fetchAccessToken.mockResolvedValueOnce(token); + getURLFromAttachments.mockResolvedValueOnce(response); + readAttachment.mockImplementationOnce(() => { + throw new Error("Error reading attachment"); + }); + + + + await expect(service.get(attachments, keys,req)).rejects.toThrow( + "Error reading attachment" + ); + + expect(getURLFromAttachments).toHaveBeenCalledWith(keys, attachments); + expect(fetchAccessToken).toHaveBeenCalledWith(service.creds, 'tokenValue'); + expect(readAttachment).toHaveBeenCalledWith( + "mockUrl", + token, + service.creds + ); + }); }); describe("draftSaveHandler", () => { let service; @@ -117,6 +135,11 @@ describe("SDMAttachmentsService", () => { name: "testName", }, }, + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('mocked_token'), + }, + }, reject: jest.fn(), info: jest.fn(), }; @@ -148,7 +171,7 @@ describe("SDMAttachmentsService", () => { await service.draftSaveHandler(mockReq); expect(getDraftAttachments).toHaveBeenCalledTimes(1); expect(service.isFileNameDuplicateInDrafts).toHaveBeenCalledTimes(1); - expect(fetchAccessToken).toHaveBeenCalledWith(service.creds); + expect(fetchAccessToken).toHaveBeenCalledWith(service.creds, 'mocked_token'); expect(getFolderIdForEntity).toHaveBeenCalledTimes(1); expect(getFolderIdByPath).toHaveBeenCalledWith( mockReq, @@ -193,6 +216,7 @@ describe("SDMAttachmentsService", () => { it("should handle failure in onCreate", async () => { service.isFileNameDuplicate = jest.fn().mockResolvedValue(""); service.onCreate = jest.fn().mockResolvedValue(["ChildTest"]); + getDraftAttachments.mockResolvedValue([{}]); await service.draftSaveHandler(mockReq); @@ -203,7 +227,6 @@ describe("SDMAttachmentsService", () => { it("should not call onCreate if no draft attachments are available", async () => { getDraftAttachments.mockResolvedValue([]); const onCreateSpy = jest.spyOn(service, "onCreate"); - await service.draftSaveHandler(mockReq); expect(onCreateSpy).not.toBeCalled(); @@ -225,7 +248,6 @@ describe("SDMAttachmentsService", () => { { name: "Attachment#1" }, { name: "Attachment#2" }, ]; - // Mock method return values getDraftAttachments.mockResolvedValue(mockAttachments); service.isFileNameDuplicateInDrafts = jest @@ -260,11 +282,17 @@ describe("SDMAttachmentsService", () => { service = new SDMAttachmentsService(); }); it("should add attachments to delete in req when deletions are present", async () => { + const mockedReq = { query: { target: { name: "myName", }, + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, + }, }, diff: jest.fn().mockResolvedValue({ attachments: [ @@ -281,7 +309,6 @@ describe("SDMAttachmentsService", () => { "attachment3", "attachment4", ]); - await service.attachDeletionData(mockedReq); expect(mockedReq.diff).toHaveBeenCalled(); expect(getURLsToDeleteFromAttachments).toHaveBeenCalledWith( @@ -300,6 +327,11 @@ describe("SDMAttachmentsService", () => { target: { name: "myName", }, + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, + }, }, diff: jest.fn().mockResolvedValue({ attachments: [], @@ -308,7 +340,6 @@ describe("SDMAttachmentsService", () => { }; const mockedAttachments = ["attachment3", "attachment4"]; cds.model.definitions["myName.attachments"] = mockedAttachments; - await service.attachDeletionData(mockedReq); expect(mockedReq.diff).toHaveBeenCalled(); expect(getURLsToDeleteFromAttachments).not.toHaveBeenCalled(); @@ -320,6 +351,10 @@ describe("SDMAttachmentsService", () => { query: { target: { name: "myName", + },user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, }, }, diff: jest.fn().mockResolvedValue({ @@ -329,7 +364,6 @@ describe("SDMAttachmentsService", () => { }; const mockedAttachments = []; cds.model.definitions["myName.attachments"] = mockedAttachments; - await service.attachDeletionData(mockedReq); expect(mockedReq.diff).toHaveBeenCalled(); expect(getURLsToDeleteFromAttachments).not.toHaveBeenCalled(); @@ -338,11 +372,17 @@ describe("SDMAttachmentsService", () => { it("attachDeletionData() should set req.parentId if event is DELETE and getFolderIdForEntity() returns non-empty array", async () => { const mockReq = { - query: { target: { name: "testName" } }, + query: { target: { name: "testName" }, + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, + }, }, diff: () => Promise.resolve({ attachments: [{ _op: "delete", ID: "1" }] }), event: "DELETE", }; + getURLsToDeleteFromAttachments.mockResolvedValueOnce(["url"]); getFolderIdForEntity.mockResolvedValueOnce([{ folderId: "folder" }]); await service.attachDeletionData(mockReq); @@ -352,11 +392,17 @@ describe("SDMAttachmentsService", () => { it("attachDeletionData() should not set req.parentId if event is DELETE and getFolderIdForEntity() returns empty array", async () => { const mockReq = { - query: { target: { name: "testName" } }, + query: { target: { name: "testName" }, + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, + }, }, diff: () => Promise.resolve({ attachments: [{ _op: "delete", ID: "1" }] }), event: "DELETE", }; + getURLsToDeleteFromAttachments.mockResolvedValueOnce(["url"]); getFolderIdForEntity.mockResolvedValueOnce([]); await service.attachDeletionData(mockReq); @@ -366,11 +412,17 @@ describe("SDMAttachmentsService", () => { it("attachDeletionData() should not call getFolderIdForEntity() if event is not DELETE", async () => { const mockReq = { - query: { target: { name: "testName" } }, + query: { target: { name: "testName" }, + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, + }, }, diff: () => Promise.resolve({ attachments: [{ _op: "delete", ID: "1" }] }), event: "CREATE", }; + getURLsToDeleteFromAttachments.mockResolvedValueOnce(["url"]); await service.attachDeletionData(mockReq); expect(getFolderIdForEntity).toHaveBeenCalledTimes(0); @@ -380,13 +432,17 @@ describe("SDMAttachmentsService", () => { query: { target: { name: "testName" }, }, + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, + }, 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 @@ -397,6 +453,11 @@ describe("SDMAttachmentsService", () => { it("attachDeletionData() should not set req.attachmentsToDelete if there are no attachments to delete", async () => { const mockReq = { query: { target: { name: "testName" } }, + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, + }, diff: () => Promise.resolve({ attachments: [{ _op: "delete", ID: "1" }] }), }; @@ -422,7 +483,13 @@ describe("SDMAttachmentsService", () => { { url: "test_url2", ID: "2" }, ], info: jest.fn(), + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, + }, }; + const expectedErrorResponse = "test_error_response"; cds.model.definitions["testTarget.attachments"] = {}; // Add relevant attachment definition @@ -431,7 +498,6 @@ describe("SDMAttachmentsService", () => { service.handleRequest = jest .fn() .mockResolvedValueOnce({ message: expectedErrorResponse, ID: "2" }); - await service.deleteAttachmentsWithKeys(records, req); expect(fetchAccessToken).toHaveBeenCalledTimes(1); @@ -448,6 +514,11 @@ describe("SDMAttachmentsService", () => { const req = { query: { target: { name: "testTarget" } }, attachmentsToDelete: [], + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, + }, }; await service.deleteAttachmentsWithKeys(records, req); @@ -462,6 +533,11 @@ describe("SDMAttachmentsService", () => { query: { target: { name: "testName" } }, attachmentsToDelete: ["file1", "file2"], parentId: "some_folder_id", + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, + }, }; fetchAccessToken.mockResolvedValueOnce("mocked_token"); @@ -469,7 +545,7 @@ describe("SDMAttachmentsService", () => { await service.deleteAttachmentsWithKeys([], mockReq); - expect(fetchAccessToken).toHaveBeenCalledWith(service.creds); + expect(fetchAccessToken).toHaveBeenCalledWith(service.creds, 'tokenValue'); expect(deleteFolderWithAttachments).toHaveBeenCalledWith( service.creds, "mocked_token", @@ -490,7 +566,12 @@ describe("SDMAttachmentsService", () => { const credentials = {}; const token = "token"; const attachments = []; - const req = { data: { attachments: [...data] } }; + const req = { data: { attachments: [...data] }, + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, + }, }; createAttachment.mockResolvedValue({ status: 201, @@ -514,7 +595,12 @@ describe("SDMAttachmentsService", () => { const credentials = {}; const token = "token"; const attachments = []; - const req = { data: { attachments: [...data] } }; + const req = { data: { attachments: [...data] } , + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, + },}; createAttachment .mockResolvedValueOnce({ @@ -544,7 +630,12 @@ describe("SDMAttachmentsService", () => { { content: "valid_data", filename: "filename2", ID: "id2" }, ]; const mockReq = { - data: { attachments: [...mockAttachments] }, + data: { attachments: [...mockAttachments] , + user: { + tokenInfo: { + getTokenValue: jest.fn().mockReturnValue('tokenValue'), + }, + },}, }; const token = "mocked_token"; const credentials = "mocked_credentials"; diff --git a/test/lib/util/index.test.js b/test/lib/util/index.test.js index cddbb59..8e80b30 100644 --- a/test/lib/util/index.test.js +++ b/test/lib/util/index.test.js @@ -1,10 +1,14 @@ const xssec = require("@sap/xssec"); const NodeCache = require("node-cache"); +const jwt = require('jsonwebtoken'); + + const { fetchAccessToken, getConfigurations, } = require("../../../lib/util/index"); const cds = require("@sap/cds"); +let dummyToken = ""; jest.mock("@sap/xssec"); jest.mock("node-cache"); @@ -13,55 +17,113 @@ jest.mock("@sap/cds"); describe("util", () => { describe("fetchAccessToken", () => { beforeEach(() => { - xssec.requests.requestClientCredentialsToken.mockClear(); + xssec.requests.requestUserToken.mockClear(); NodeCache.prototype.get.mockClear(); NodeCache.prototype.set.mockClear(); + const payload = { + "sub": "1234567890", + "email": "example@example.com", + "exp": 1516239022 + }; + + // Please replace 'your_secret_key' with your own secret key + const secretKey = 'your_secret_key'; + + // sign the token with your secret key + dummyToken = jwt.sign(payload, secretKey); + }); - it("requestClientCredentialsToken should be called when no token in cache", async () => { + it("requestUserToken should be called when no token in cache", async () => { NodeCache.prototype.get.mockImplementation(() => undefined); - xssec.requests.requestClientCredentialsToken.mockImplementation( - (a, b, c, callback) => callback(null, "new token") + xssec.requests.requestUserToken.mockImplementation( + (a, b, c, d, e, f, callback) => callback(null, dummyToken) ); const credentials = { uaa: "uaa" }; - const accessToken = await fetchAccessToken(credentials); - expect(NodeCache.prototype.get).toBeCalledWith("SDM_ACCESS_TOKEN"); - expect(xssec.requests.requestClientCredentialsToken).toBeCalled(); + const req = { + user: { + tokenInfo: { + getTokenValue: dummyToken, + }, + }, + }; + const accessToken = await fetchAccessToken(credentials, req.user.tokenInfo.getTokenValue); + expect(NodeCache.prototype.get).toBeCalledWith("example@example.com"); + expect(xssec.requests.requestUserToken).toBeCalled(); expect(NodeCache.prototype.set).toBeCalledWith( - "SDM_ACCESS_TOKEN", - "new token", - 11 + "example@example.com", + dummyToken, + 11 * 3600 ); - expect(accessToken).toBe("new token"); + expect(accessToken).toBe(dummyToken); + }); + + it("requestUserToken should not be called when there is already token in cache which is expired", async () => { + NodeCache.prototype.get.mockImplementation(() => dummyToken); + const req = { + user: { + tokenInfo: { + getTokenValue: dummyToken, + }, + }, + }; + const credentials = { uaa: "uaa" }; + const accessToken = await fetchAccessToken(credentials, req.user.tokenInfo.getTokenValue); + expect(NodeCache.prototype.get).toBeCalledWith("example@example.com"); + expect(xssec.requests.requestUserToken).toBeCalled(); + expect(accessToken).toBe(dummyToken); }); - it("requestClientCredentialsToken should not be called when there is already token in cache", async () => { - NodeCache.prototype.get.mockImplementation(() => "token"); + it("requestUserToken should be called when there is already token in cache which is not expired", async () => { + payload = { + "sub": "1234567890", + "email": "example@example.com", + "exp": 2537353178 + }; + + // Please replace 'your_secret_key' with your own secret key + const secretKey = 'your_secret_key'; + // sign the token with your secret key + dummyToken = jwt.sign(payload, secretKey); + NodeCache.prototype.get.mockImplementation(() => dummyToken); + const req = { + user: { + tokenInfo: { + getTokenValue: dummyToken, + }, + }, + }; const credentials = { uaa: "uaa" }; - const accessToken = await fetchAccessToken(credentials); - expect(NodeCache.prototype.get).toBeCalledWith("SDM_ACCESS_TOKEN"); - expect(xssec.requests.requestClientCredentialsToken).not.toBeCalled(); - expect(accessToken).toBe("token"); + const accessToken = await fetchAccessToken(credentials, req.user.tokenInfo.getTokenValue); + expect(NodeCache.prototype.get).toBeCalledWith("example@example.com"); + expect(xssec.requests.requestUserToken).not.toBeCalled(); + expect(accessToken).toBe(dummyToken); }); it("should throw error when request for access token fails", async () => { const consoleErrorSpy = jest .spyOn(console, "error") - .mockImplementation(() => {}); + .mockImplementation(() => { }); NodeCache.prototype.get.mockImplementationOnce(() => undefined); - xssec.requests.requestClientCredentialsToken.mockImplementation( - (a, b, c, callback) => + xssec.requests.requestUserToken.mockImplementation( + (a, b, c, d, e, f, callback) => callback(new Error("test error"), { statusCode: 500 }) ); - + const req = { + user: { + tokenInfo: { + getTokenValue: dummyToken, + }, + }, + }; const credentials = { uaa: "uaa" }; try { - await fetchAccessToken(credentials); + await fetchAccessToken(credentials, req.user.tokenInfo.getTokenValue); } catch (err) { - expect(NodeCache.prototype.get).toBeCalledWith("SDM_ACCESS_TOKEN"); - expect(xssec.requests.requestClientCredentialsToken).toBeCalled(); + expect(NodeCache.prototype.get).toBeCalledWith("example@example.com"); + expect(xssec.requests.requestUserToken).toBeCalled(); expect(consoleErrorSpy).toBeCalledWith( "Response error while fetching access token 500" );