From 1afbe8bb70cdae91f99c1ee3dab0bb6b98325ac0 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Wed, 23 Apr 2025 14:28:28 +0800 Subject: [PATCH 01/21] refactor --- jest.config.js | 3 +- src/commands/app/deploy.js | 4 +- src/lib/app-helper.js | 66 ++++++++- src/lib/audit-logger.js | 183 ++++++++++++++----------- test/commands/app/deploy.test.js | 4 +- test/commands/lib/app-helper.test.js | 71 +++++++++- test/commands/lib/audit-logger.test.js | 152 ++++---------------- test/jest.setup.js | 2 +- 8 files changed, 265 insertions(+), 220 deletions(-) diff --git a/jest.config.js b/jest.config.js index 05c8f8675..a8176778c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -27,7 +27,8 @@ module.exports = { global: { branches: 100, lines: 100, - statements: 100 + statements: 100, + functions: 100 } } } diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index a8914d998..7420171cc 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -18,10 +18,10 @@ const BaseCommand = require('../../BaseCommand') const BuildCommand = require('./build') const webLib = require('@adobe/aio-lib-web') const { Flags } = require('@oclif/core') -const { runInProcess, buildExtensionPointPayloadWoMetadata, buildExcShellViewExtensionMetadata, getCliInfo } = require('../../lib/app-helper') +const { runInProcess, buildExtensionPointPayloadWoMetadata, buildExcShellViewExtensionMetadata, getCliInfo, getFilesCountWithExtension } = require('../../lib/app-helper') const rtLib = require('@adobe/aio-lib-runtime') const LogForwarding = require('../../lib/log-forwarding') -const { sendAuditLogs, getAuditLogEvent, getFilesCountWithExtension } = require('../../lib/audit-logger') +const { sendAuditLogs, getAuditLogEvent } = require('../../lib/audit-logger') const { setRuntimeApiHostAndAuthHandler } = require('../../lib/auth-helper') const logActions = require('../../lib/log-actions') diff --git a/src/lib/app-helper.js b/src/lib/app-helper.js index dd5025233..4b236e900 100644 --- a/src/lib/app-helper.js +++ b/src/lib/app-helper.js @@ -11,7 +11,7 @@ governing permissions and limitations under the License. const execa = require('execa') const fs = require('fs-extra') -const path = require('path') +const path = require('node:path') const which = require('which') const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-app:lib-app-helper', { provider: 'debug' }) const { getToken, context } = require('@adobe/aio-lib-ims') @@ -473,6 +473,67 @@ function getObjectValue (obj, key) { return keys.filter(o => o.trim()).reduce((o, i) => o && getObjectProp(o, i), obj) } +/** + * Counts files by extension in a directory + * + * @param {string} directory Path to assets directory + * @returns {Array} Array of formatted log messages + */ +function getFilesCountWithExtension (directory) { + const log = [] + + if (!fs.existsSync(directory)) { + throw new Error(`Error: Directory ${directory} does not exist.`) + } + + const files = fs.readdirSync(directory, { recursive: true }) + if (files.length === 0) { + throw new Error(`Error: No files found in directory ${directory}.`) + } + + const fileTypeCounts = {} + files.forEach(file => { + const ext = path.extname(file).toLowerCase() || 'no extension' + if (fileTypeCounts[ext]) { + fileTypeCounts[ext]++ + } else { + fileTypeCounts[ext] = 1 + } + }) + + Object.keys(fileTypeCounts).forEach(ext => { + const count = fileTypeCounts[ext] + let description + switch (ext) { + case '.js': + description = 'Javascript file(s)' + break + case '.css': + description = 'CSS file(s)' + break + case '.html': + description = 'HTML page(s)' + break + case '.png': + case '.jpg': + case '.jpeg': + case '.gif': + case '.svg': + case '.webp': + description = `${ext} image(s)` + break + case 'no extension': + description = 'file(s) without extension' + break + default: + description = `${ext} file(s)` + } + log.push(`${count} ${description}\n`) + }) + + return log +} + module.exports = { getObjectValue, getObjectProp, @@ -496,5 +557,6 @@ module.exports = { buildExtensionPointPayloadWoMetadata, buildExcShellViewExtensionMetadata, atLeastOne, - deleteUserConfig + deleteUserConfig, + getFilesCountWithExtension } diff --git a/src/lib/audit-logger.js b/src/lib/audit-logger.js index 578fd583b..73c192f16 100644 --- a/src/lib/audit-logger.js +++ b/src/lib/audit-logger.js @@ -16,7 +16,6 @@ const chalk = require('chalk') const OPERATIONS = { AB_APP_DEPLOY: 'ab_app_deploy', AB_APP_UNDEPLOY: 'ab_app_undeploy', - AB_APP_TEST: 'ab_app_test', AB_APP_ASSETS_DEPLOYED: 'ab_app_assets_deployed', AB_APP_ASSETS_UNDEPLOYED: 'ab_app_assets_undeployed' } @@ -26,6 +25,7 @@ const AUDIT_SERVICE_ENDPOINTS = { prod: 'https://adp-auditlog-service-prod.adobeioruntime.net/api/v1/web/audit-log-api/event-post' } + /** * Send audit log events to audit service * @param {string} accessToken valid access token @@ -53,106 +53,127 @@ async function sendAuditLogs (accessToken, logEvent, env = 'prod') { } /** + * Send audit log event for app deployment * - * @param {object} flags cli flags - * @param {object} project details - * @param {string} event log name - * @returns {object} logEvent + * @param {Object} params Parameters object + * @param {string} params.accessToken valid access token + * @param {object} params.cliCommandFlags cli flags + * @param {object} params.project project details + * @param {string} [params.env='prod'] valid env stage|prod */ -function getAuditLogEvent (flags, project, event) { - let logEvent, logStrMsg - if (project && project.org && project.workspace) { - if (event === 'AB_APP_DEPLOY') { - logStrMsg = `Starting deployment for the App Builder application in workspace ${project.workspace.name}` - } else if (event === 'AB_APP_UNDEPLOY') { - logStrMsg = `Starting undeployment for the App Builder application in workspace ${project.workspace.name}` - } else if (event === 'AB_APP_ASSETS_UNDEPLOYED') { - logStrMsg = `All static assets for the App Builder application in workspace: ${project.workspace.name} were successfully undeployed from the CDN` - } else if (event === 'AB_APP_ASSETS_DEPLOYED') { - logStrMsg = `All static assets for the App Builder application in workspace: ${project.workspace.name} were successfully deployed to the CDN.\n Files deployed - ` - } +async function sendAppDeployAuditLog ({ accessToken, cliCommandFlags, project, env }) { + const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_DEPLOY) + return sendAuditLogs(accessToken, logEvent, env) +} - logEvent = { - orgId: project.org.id, - projectId: project.id, - workspaceId: project.workspace.id, - workspaceName: project.workspace.name, - operation: event in OPERATIONS ? OPERATIONS[event] : OPERATIONS.AB_APP_TEST, - timestamp: new Date().valueOf(), - data: { - cliCommandFlags: flags, - opDetailsStr: logStrMsg - } - } - } - return logEvent +/** + * Send audit log event for app undeployment + * + * @param {Object} params Parameters object + * @param {string} params.accessToken valid access token + * @param {object} params.cliCommandFlags cli flags + * @param {object} params.project project details + * @param {string} [params.env='prod'] valid env stage|prod + */ +async function sendAppUndeployAuditLog ({ accessToken, cliCommandFlags, project, env }) { + const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_UNDEPLOY) + return sendAuditLogs(accessToken, logEvent, env) } /** + * Send audit log event for app assets deployment * - * @param {string} directory | path to assets directory - * @returns {Array} log | array of log messages + * @param {Object} params Parameters object + * @param {string} params.accessToken valid access token + * @param {object} params.cliCommandFlags cli flags + * @param {object} params.project project details + * @param {Array} params.opItems list of deployed files + * @param {string} [params.env='prod'] valid env stage|prod */ -function getFilesCountWithExtension (directory) { - const log = [] +async function sendAppAssetsDeployedAuditLog ({ accessToken, cliCommandFlags, project, opItems, env }) { + const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_ASSETS_DEPLOYED) + logEvent.data.opItems = opItems + return sendAuditLogs(accessToken, logEvent, env) +} - if (!fs.existsSync(directory)) { - this.log(chalk.red(chalk.bold(`Error: Directory ${directory} does not exist.`))) - return log +/** + * Send audit log event for app assets undeployment + * + * @param {Object} params Parameters object + * @param {string} params.accessToken valid access token + * @param {object} params.cliCommandFlags cli flags + * @param {object} params.project project details + * @param {string} [params.env='prod'] valid env stage|prod + */ +async function sendAppAssetsUndeployedAuditLog ({ accessToken, cliCommandFlags, project, env }) { + const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_ASSETS_UNDEPLOYED) + return sendAuditLogs(accessToken, logEvent, env) +} + +/** + * + * @param {object} cliCommandFlags cli flags + * @param {object} project details + * @param {string} operation one of: ab_app_deploy, ab_app_undeploy, ab_app_assets_deployed, ab_app_assets_undeployed + * @returns {object} logEvent + */ +function getAuditLogEvent (cliCommandFlags, project, operation) { + if (!project) { + throw new Error('Project is required') + } + if (!project.org) { + throw new Error('Project org is required') + } + if (!project.workspace) { + throw new Error('Project workspace is required') } - const files = fs.readdirSync(directory, { recursive: true }) + const workspaceName = project.workspace.name - if (files.length === 0) { - this.log(chalk.red(chalk.bold(`Error: No files found in directory ${directory}.`))) - return log + let logStrMsg + switch (operation) { + case OPERATIONS.AB_APP_DEPLOY: + logStrMsg = `Starting deployment for the App Builder application in workspace ${workspaceName}` + break + case OPERATIONS.AB_APP_UNDEPLOY: + logStrMsg = `Starting undeployment for the App Builder application in workspace ${workspaceName}` + break + case OPERATIONS.AB_APP_ASSETS_UNDEPLOYED: + logStrMsg = `All static assets for the App Builder application in workspace: ${workspaceName} were successfully undeployed from the CDN` + break + case OPERATIONS.AB_APP_ASSETS_DEPLOYED: + logStrMsg = `All static assets for the App Builder application in workspace: ${workspaceName} were successfully deployed to the CDN.\n Files deployed - ` + break + default: + throw new Error(`Invalid operation: ${operation}`) } - const fileTypeCounts = {} - files.forEach(file => { - const ext = path.extname(file).toLowerCase() || 'no extension' - if (fileTypeCounts[ext]) { - fileTypeCounts[ext]++ - } else { - fileTypeCounts[ext] = 1 - } - }) + const orgId = project.org.id + const projectId = project.id + const workspaceId = project.workspace.id - Object.keys(fileTypeCounts).forEach(ext => { - const count = fileTypeCounts[ext] - let description - switch (ext) { - case '.js': - description = 'Javascript file(s)' - break - case '.css': - description = 'CSS file(s)' - break - case '.html': - description = 'HTML page(s)' - break - case '.png': - case '.jpg': - case '.jpeg': - case '.gif': - case '.svg': - case '.webp': - description = `${ext} image(s)` - break - case 'no extension': - description = 'file(s) without extension' - break - default: - description = `${ext} file(s)` + const logEvent = { + orgId, + projectId, + workspaceId, + workspaceName, + operation, + timestamp: new Date().valueOf(), + data: { + cliCommandFlags, + opDetailsStr: logStrMsg } - log.push(`${count} ${description}\n`) - }) - return log + } + return logEvent } module.exports = { + OPERATIONS, sendAuditLogs, getAuditLogEvent, AUDIT_SERVICE_ENDPOINTS, - getFilesCountWithExtension + sendAppDeployAuditLog, + sendAppUndeployAuditLog, + sendAppAssetsDeployedAuditLog, + sendAppAssetsUndeployedAuditLog } diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index e0fed79d5..b29b3765e 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -186,7 +186,7 @@ beforeEach(() => { } } }) - auditLogger.getFilesCountWithExtension.mockImplementation((dir) => { + helpers.getFilesCountWithExtension.mockImplementation((dir) => { return [ '3 Javascript file(s)', '2 CSS file(s)', @@ -1479,7 +1479,7 @@ describe('run', () => { expect(command.error).toHaveBeenCalledTimes(0) expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) - expect(auditLogger.getFilesCountWithExtension).toHaveBeenCalledTimes(2) + expect(helpers.getFilesCountWithExtension).toHaveBeenCalledTimes(2) expect(auditLogger.sendAuditLogs).toHaveBeenCalledWith(mockToken, expect.objectContaining({ orgId: mockOrg, projectId: mockProject, workspaceId: mockWorkspaceId, workspaceName: mockWorkspaceName }), mockEnv) }) diff --git a/test/commands/lib/app-helper.test.js b/test/commands/lib/app-helper.test.js index 1e6af12f9..1ddd97e1c 100644 --- a/test/commands/lib/app-helper.test.js +++ b/test/commands/lib/app-helper.test.js @@ -17,7 +17,7 @@ const mockFetch = jest.fn() jest.mock('@adobe/aio-lib-core-config') jest.mock('execa') -jest.mock('path') +jest.mock('node:path') jest.mock('fs-extra') // do not touch the real fs jest.mock('@adobe/aio-lib-env') jest.mock('@adobe/aio-lib-ims') @@ -25,7 +25,7 @@ jest.mock('@adobe/aio-lib-ims') const mockLogger = require('@adobe/aio-lib-core-logging') const which = require('which') -const path = require('path') +const path = require('node:path') const fs = require('fs-extra') const execa = require('execa') const appHelper = require('../../../src/lib/app-helper') @@ -34,6 +34,9 @@ const libEnv = require('@adobe/aio-lib-env') const libIms = require('@adobe/aio-lib-ims') beforeEach(() => { + // use actual implementation + path.extname.mockImplementation(jest.requireActual('node:path').extname) + Object.defineProperty(process, 'platform', { value: 'linux' }) execa.mockReset() execa.command.mockReset() @@ -806,3 +809,67 @@ describe('object values', () => { expect(appHelper.getObjectValue(obj)).toEqual(obj) }) }) + +describe('getFilesCountWithExtension', () => { + const directory = '__fixtures__/app/web-src' + + it('should return an error message when directory does not exist', () => { + fs.existsSync.mockReturnValue(false) + + expect(() => appHelper.getFilesCountWithExtension(directory)).toThrow(`Error: Directory ${directory} does not exist.`) + expect(fs.existsSync).toHaveBeenCalledWith(directory) + }) + + it('should return an error message when directory is empty', () => { + fs.existsSync.mockReturnValue(true) + fs.readdirSync.mockReturnValue([]) + + expect(() => appHelper.getFilesCountWithExtension(directory)).toThrow(`Error: No files found in directory ${directory}.`) + expect(fs.existsSync).toHaveBeenCalledWith(directory) + expect(fs.readdirSync).toHaveBeenCalledWith(directory, { recursive: true }) + }) + + it('should return a count of different file types', () => { + fs.existsSync.mockReturnValue(true) + fs.readdirSync.mockReturnValue(['index.html', 'script.js', 'styles.css', 'image.png', 'image.jpg', 'readme']) + + const result = appHelper.getFilesCountWithExtension(directory) + // this really should be 2 image(s) but there is a side effect in the code that makes it split by ext + // and this makes more sense than seeing 1 image(s), 1 image(s) + expect(result).toEqual([ + '1 HTML page(s)\n', + '1 Javascript file(s)\n', + '1 CSS file(s)\n', + '1 .png image(s)\n', + '1 .jpg image(s)\n', + '1 file(s) without extension\n' + ]) + }) + + it('should handle directories with files of the same type', () => { + fs.existsSync.mockReturnValue(true) + fs.readdirSync.mockReturnValue(['script1.js', 'script2.js', 'script3.js']) + + const result = appHelper.getFilesCountWithExtension(directory) + expect(result).toEqual(['3 Javascript file(s)\n']) + }) + + it('should handle files with no extension', () => { + fs.existsSync.mockReturnValue(true) + fs.readdirSync.mockReturnValue(['readme', 'LICENSE']) + + const result = appHelper.getFilesCountWithExtension(directory) + expect(result).toEqual(['2 file(s) without extension\n']) + }) + + it('should handle files with other extensions', () => { + fs.existsSync.mockReturnValue(true) + fs.readdirSync.mockReturnValue(['data.json', 'document.pdf']) + + const result = appHelper.getFilesCountWithExtension(directory) + expect(result).toEqual([ + '1 .json file(s)\n', + '1 .pdf file(s)\n' + ]) + }) +}) diff --git a/test/commands/lib/audit-logger.test.js b/test/commands/lib/audit-logger.test.js index 333420998..036388c95 100644 --- a/test/commands/lib/audit-logger.test.js +++ b/test/commands/lib/audit-logger.test.js @@ -10,10 +10,8 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const fs = require('fs') -/* eslint-disable no-unused-vars */ -const path = require('path') -/* eslint-disable no-unused-vars */ +const fs = require('node:fs') +const path = require('node:path') const chalk = require('chalk') const fetch = require('node-fetch') jest.mock('node-fetch', () => jest.fn()) @@ -26,14 +24,6 @@ jest.mock('chalk', () => ({ bold: jest.fn((text) => text) })) -const OPERATIONS = { - AB_APP_DEPLOY: 'ab_app_deploy', - AB_APP_UNDEPLOY: 'ab_app_undeploy', - AB_APP_ASSETS_UNDEPLOYED: 'ab_app_assets_undeployed', - AB_APP_ASSETS_DEPLOYED: 'ab_app_assets_deployed', - AB_APP_TEST: 'ab_app_test' -} - const mockToken = 'mocktoken' const mockEnv = 'stage' const mockLogEvent = { @@ -123,7 +113,7 @@ test('sendAuditLogs error response', async () => { }) describe('getAuditLogEvent', () => { - const flags = { flag1: 'value1' } + const cliCommandFlags = { flag1: 'value1' } const project = { org: { id: 'org123' }, id: 'proj456', @@ -134,182 +124,86 @@ describe('getAuditLogEvent', () => { const mockUndeployMessage = 'Starting undeployment for the App Builder application in workspace testWorkspace' test('should return correct log event for AB_APP_DEPLOY event', () => { - const event = 'AB_APP_DEPLOY' - const result = auditLogger.getAuditLogEvent(flags, project, event) + const result = auditLogger.getAuditLogEvent(cliCommandFlags, project, auditLogger.OPERATIONS.AB_APP_DEPLOY) expect(result).toEqual({ orgId: 'org123', projectId: 'proj456', workspaceId: 'ws789', workspaceName: 'testWorkspace', - operation: OPERATIONS.AB_APP_DEPLOY, + operation: auditLogger.OPERATIONS.AB_APP_DEPLOY, timestamp: expect.any(Number), data: { - cliCommandFlags: flags, + cliCommandFlags, opDetailsStr: mockDeployMessage } }) }) test('should return correct log event for AB_APP_UNDEPLOY event', () => { - const event = 'AB_APP_UNDEPLOY' - const result = auditLogger.getAuditLogEvent(flags, project, event) + const result = auditLogger.getAuditLogEvent(cliCommandFlags, project, auditLogger.OPERATIONS.AB_APP_UNDEPLOY) expect(result).toEqual({ orgId: 'org123', projectId: 'proj456', workspaceId: 'ws789', workspaceName: 'testWorkspace', - operation: OPERATIONS.AB_APP_UNDEPLOY, + operation: auditLogger.OPERATIONS.AB_APP_UNDEPLOY, timestamp: expect.any(Number), data: { - cliCommandFlags: flags, + cliCommandFlags, opDetailsStr: mockUndeployMessage } }) }) test('should return correct log event for AB_APP_ASSETS_UNDEPLOYED event', () => { - const event = 'AB_APP_ASSETS_UNDEPLOYED' - const result = auditLogger.getAuditLogEvent(flags, project, event) + const result = auditLogger.getAuditLogEvent(cliCommandFlags, project, auditLogger.OPERATIONS.AB_APP_ASSETS_UNDEPLOYED) expect(result).toEqual({ orgId: 'org123', projectId: 'proj456', workspaceId: 'ws789', workspaceName: 'testWorkspace', - operation: OPERATIONS.AB_APP_ASSETS_UNDEPLOYED, + operation: auditLogger.OPERATIONS.AB_APP_ASSETS_UNDEPLOYED, timestamp: expect.any(Number), data: { - cliCommandFlags: flags, + cliCommandFlags, opDetailsStr: 'All static assets for the App Builder application in workspace: testWorkspace were successfully undeployed from the CDN' } }) }) test('should return correct log event for AB_APP_ASSETS_DEPLOYED event', () => { - const event = 'AB_APP_ASSETS_DEPLOYED' - const result = auditLogger.getAuditLogEvent(flags, project, event) + const result = auditLogger.getAuditLogEvent(cliCommandFlags, project, auditLogger.OPERATIONS.AB_APP_ASSETS_DEPLOYED) expect(result).toEqual({ orgId: 'org123', projectId: 'proj456', workspaceId: 'ws789', workspaceName: 'testWorkspace', - operation: OPERATIONS.AB_APP_ASSETS_DEPLOYED, + operation: auditLogger.OPERATIONS.AB_APP_ASSETS_DEPLOYED, timestamp: expect.any(Number), data: { - cliCommandFlags: flags, + cliCommandFlags, opDetailsStr: 'All static assets for the App Builder application in workspace: testWorkspace were successfully deployed to the CDN.\n Files deployed - ' } }) }) - test('should return undefined if project or workspace is missing', () => { - const event = 'AB_APP_DEPLOY' - const result = auditLogger.getAuditLogEvent(flags, {}, event) - - expect(result).toBeFalsy() + test('should throw error if project is missing', () => { + expect(() => auditLogger.getAuditLogEvent(cliCommandFlags, null, auditLogger.OPERATIONS.AB_APP_DEPLOY)).toThrow('Project is required') }) - test('should default operation to APP_TEST if event is not found in OPERATIONS', () => { - const event = 'UNKNOWN_EVENT' - const result = auditLogger.getAuditLogEvent(flags, project, event) - - expect(result).toEqual({ - orgId: 'org123', - projectId: 'proj456', - workspaceId: 'ws789', - workspaceName: 'testWorkspace', - operation: OPERATIONS.AB_APP_TEST, - timestamp: expect.any(Number), - data: { - cliCommandFlags: flags, - opDetailsStr: undefined - } - }) + test('should throw error if project org is missing', () => { + expect(() => auditLogger.getAuditLogEvent(cliCommandFlags, {}, auditLogger.OPERATIONS.AB_APP_DEPLOY)).toThrow('Project org is required') }) -}) - -describe('getFilesCountWithExtension', () => { - const directory = '__fixtures__/app/web-src' - // Mock 'this.log' - const mockLog = jest.fn() - - beforeEach(() => { - mockLog.mockClear() // Clear mock between tests + test('should throw error if project workspace is missing', () => { + expect(() => auditLogger.getAuditLogEvent(cliCommandFlags, { org: { id: 'org123' } }, auditLogger.OPERATIONS.AB_APP_DEPLOY)).toThrow('Project workspace is required') }) - it('should return an error message when directory does not exist', () => { - fs.existsSync.mockReturnValue(false) - - const result = auditLogger.getFilesCountWithExtension.call({ log: mockLog }, directory) - - expect(fs.existsSync).toHaveBeenCalledWith(directory) - expect(mockLog).toHaveBeenCalledWith( - 'Error: Directory __fixtures__/app/web-src does not exist.' - ) - expect(result).toEqual([]) - }) - - it('should return an error message when directory is empty', () => { - fs.existsSync.mockReturnValue(true) - fs.readdirSync.mockReturnValue([]) - - const result = auditLogger.getFilesCountWithExtension.call({ log: mockLog }, directory) - - expect(fs.readdirSync).toHaveBeenCalledWith(directory, { recursive: true }) - expect(mockLog).toHaveBeenCalledWith( - 'Error: No files found in directory __fixtures__/app/web-src.' - ) - expect(result).toEqual([]) - }) - - it('should return a count of different file types', () => { - fs.existsSync.mockReturnValue(true) - fs.readdirSync.mockReturnValue(['index.html', 'script.js', 'styles.css', 'image.png', 'image.jpg', 'readme']) - - const result = auditLogger.getFilesCountWithExtension.call({ log: mockLog }, directory) - // this really should be 2 image(s) but there is a side effect in the code that makes it split by ext - // and this makes more sense than seeing 1 image(s), 1 image(s) - expect(result).toEqual([ - '1 HTML page(s)\n', - '1 Javascript file(s)\n', - '1 CSS file(s)\n', - '1 .png image(s)\n', - '1 .jpg image(s)\n', - '1 file(s) without extension\n' - ]) - }) - - it('should handle directories with files of the same type', () => { - fs.existsSync.mockReturnValue(true) - fs.readdirSync.mockReturnValue(['script1.js', 'script2.js', 'script3.js']) - - const result = auditLogger.getFilesCountWithExtension.call({ log: mockLog }, directory) - - expect(result).toEqual(['3 Javascript file(s)\n']) - }) - - it('should handle files with no extension', () => { - fs.existsSync.mockReturnValue(true) - fs.readdirSync.mockReturnValue(['readme', 'LICENSE']) - - const result = auditLogger.getFilesCountWithExtension.call({ log: mockLog }, directory) - - expect(result).toEqual(['2 file(s) without extension\n']) - }) - - it('should handle files with other extensions', () => { - fs.existsSync.mockReturnValue(true) - fs.readdirSync.mockReturnValue(['data.json', 'document.pdf']) - - const result = auditLogger.getFilesCountWithExtension.call({ log: mockLog }, directory) - - expect(result).toEqual([ - '1 .json file(s)\n', - '1 .pdf file(s)\n' - ]) + test('should throw error if event is not found in OPERATIONS', () => { + expect(() => auditLogger.getAuditLogEvent(cliCommandFlags, project, 'UNKNOWN_OPERATION')).toThrow('Invalid operation: UNKNOWN_OPERATION') }) }) diff --git a/test/jest.setup.js b/test/jest.setup.js index 5f3c8f27c..6c7320a44 100644 --- a/test/jest.setup.js +++ b/test/jest.setup.js @@ -26,7 +26,7 @@ beforeEach(() => { stdout.start() stderr.start() // change this if you need to see logs from stdout - stdout.print = false + stdout.print = true }) afterEach(() => { stdout.stop(); stderr.stop() }) From bcb9b00b8ffbd12faf776339be49ce5e24295beb Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Wed, 23 Apr 2025 15:51:57 +0800 Subject: [PATCH 02/21] fix: unit tests and coverage after refactor --- src/commands/app/deploy.js | 14 +- src/commands/app/undeploy.js | 12 +- src/lib/audit-logger.js | 42 +-- test/commands/app/deploy.test.js | 116 ++++----- test/commands/app/undeploy.test.js | 53 ++-- test/commands/lib/app-helper.test.js | 2 +- test/commands/lib/audit-logger.test.js | 345 ++++++++++++------------- test/jest.setup.js | 2 +- 8 files changed, 275 insertions(+), 311 deletions(-) diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index 7420171cc..098143f06 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -21,7 +21,7 @@ const { Flags } = require('@oclif/core') const { runInProcess, buildExtensionPointPayloadWoMetadata, buildExcShellViewExtensionMetadata, getCliInfo, getFilesCountWithExtension } = require('../../lib/app-helper') const rtLib = require('@adobe/aio-lib-runtime') const LogForwarding = require('../../lib/log-forwarding') -const { sendAuditLogs, getAuditLogEvent } = require('../../lib/audit-logger') +const { sendAppAssetsDeployedAuditLog } = require('../../lib/audit-logger') const { setRuntimeApiHostAndAuthHandler } = require('../../lib/auth-helper') const logActions = require('../../lib/log-actions') @@ -106,12 +106,16 @@ class Deploy extends BuildCommand { await this.deploySingleConfig(k, v, flags, spinner) if (v.app.hasFrontend && flags['web-assets']) { const opItems = getFilesCountWithExtension(v.web.distProd) - const assetDeployedLogEvent = getAuditLogEvent(flags, aioConfig.project, 'AB_APP_ASSETS_DEPLOYED') - if (assetDeployedLogEvent && cliDetails?.accessToken) { - assetDeployedLogEvent.data.opItems = opItems + if (cliDetails?.accessToken) { try { // only send logs in case of web-assets deployment - await sendAuditLogs(cliDetails.accessToken, assetDeployedLogEvent, cliDetails.env) + await sendAppAssetsDeployedAuditLog({ + accessToken: cliDetails.accessToken, + cliCommandFlags: flags, + project: aioConfig.project, + opItems, + env: cliDetails.env + }) } catch (error) { if (flags.verbose) { this.warn('Error: Audit Log Service Error: Failed to send audit log event for deployment.') diff --git a/src/commands/app/undeploy.js b/src/commands/app/undeploy.js index d83ad879c..1cb8eb0c2 100644 --- a/src/commands/app/undeploy.js +++ b/src/commands/app/undeploy.js @@ -19,7 +19,7 @@ const BaseCommand = require('../../BaseCommand') const webLib = require('@adobe/aio-lib-web') const { runInProcess, buildExtensionPointPayloadWoMetadata, getCliInfo } = require('../../lib/app-helper') const rtLib = require('@adobe/aio-lib-runtime') -const { sendAuditLogs, getAuditLogEvent } = require('../../lib/audit-logger') +const { sendAppAssetsUndeployedAuditLog } = require('../../lib/audit-logger') const { setRuntimeApiHostAndAuthHandler } = require('../../lib/auth-helper') class Undeploy extends BaseCommand { @@ -60,11 +60,15 @@ class Undeploy extends BaseCommand { const v = process.env.IS_DEPLOY_SERVICE_ENABLED === 'true' ? setRuntimeApiHostAndAuthHandler(values[i]) : values[i] await this.undeployOneExt(k, v, flags, spinner) - const assetUndeployLogEvent = getAuditLogEvent(flags, aioConfig.project, 'AB_APP_ASSETS_UNDEPLOYED') // send logs for case of web-assets undeployment - if (assetUndeployLogEvent && cliDetails?.accessToken) { + if (cliDetails?.accessToken) { try { - await sendAuditLogs(cliDetails.accessToken, assetUndeployLogEvent, cliDetails.env) + await sendAppAssetsUndeployedAuditLog({ + accessToken: cliDetails.accessToken, + cliCommandFlags: flags, + project: aioConfig.project, + env: cliDetails.env + }) } catch (error) { this.warn('Warning: Audit Log Service Error: Failed to send audit log event for un-deployment.') } diff --git a/src/lib/audit-logger.js b/src/lib/audit-logger.js index 73c192f16..767485032 100644 --- a/src/lib/audit-logger.js +++ b/src/lib/audit-logger.js @@ -9,9 +9,6 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ const fetch = require('node-fetch') -const fs = require('fs') -const path = require('path') -const chalk = require('chalk') const OPERATIONS = { AB_APP_DEPLOY: 'ab_app_deploy', @@ -25,14 +22,16 @@ const AUDIT_SERVICE_ENDPOINTS = { prod: 'https://adp-auditlog-service-prod.adobeioruntime.net/api/v1/web/audit-log-api/event-post' } - /** * Send audit log events to audit service - * @param {string} accessToken valid access token - * @param {object} logEvent logEvent details - * @param {string} env valid env stage|prod + * + * @param {object} params Parameters object + * @param {string} params.accessToken valid access token + * @param {object} params.logEvent logEvent details + * @param {string} [params.env='prod'] valid env stage|prod + * @returns {Promise} Promise that resolves when the audit log is sent successfully */ -async function sendAuditLogs (accessToken, logEvent, env = 'prod') { +async function sendAuditLogs ({ accessToken, logEvent, env = 'prod' }) { const url = AUDIT_SERVICE_ENDPOINTS[env] ?? AUDIT_SERVICE_ENDPOINTS.prod const payload = { event: logEvent @@ -55,67 +54,73 @@ async function sendAuditLogs (accessToken, logEvent, env = 'prod') { /** * Send audit log event for app deployment * - * @param {Object} params Parameters object + * @param {object} params Parameters object * @param {string} params.accessToken valid access token * @param {object} params.cliCommandFlags cli flags * @param {object} params.project project details * @param {string} [params.env='prod'] valid env stage|prod + * @returns {Promise} Promise that resolves when the audit log is sent successfully */ async function sendAppDeployAuditLog ({ accessToken, cliCommandFlags, project, env }) { const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_DEPLOY) - return sendAuditLogs(accessToken, logEvent, env) + return sendAuditLogs({ accessToken, logEvent, env }) } /** * Send audit log event for app undeployment * - * @param {Object} params Parameters object + * @param {object} params Parameters object * @param {string} params.accessToken valid access token * @param {object} params.cliCommandFlags cli flags * @param {object} params.project project details * @param {string} [params.env='prod'] valid env stage|prod + * @returns {Promise} Promise that resolves when the audit log is sent successfully */ async function sendAppUndeployAuditLog ({ accessToken, cliCommandFlags, project, env }) { const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_UNDEPLOY) - return sendAuditLogs(accessToken, logEvent, env) + return sendAuditLogs({ accessToken, logEvent, env }) } /** * Send audit log event for app assets deployment * - * @param {Object} params Parameters object + * @param {object} params Parameters object * @param {string} params.accessToken valid access token * @param {object} params.cliCommandFlags cli flags * @param {object} params.project project details * @param {Array} params.opItems list of deployed files * @param {string} [params.env='prod'] valid env stage|prod + * @returns {Promise} Promise that resolves when the audit log is sent successfully */ async function sendAppAssetsDeployedAuditLog ({ accessToken, cliCommandFlags, project, opItems, env }) { const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_ASSETS_DEPLOYED) logEvent.data.opItems = opItems - return sendAuditLogs(accessToken, logEvent, env) + return sendAuditLogs({ accessToken, logEvent, env }) } /** * Send audit log event for app assets undeployment * - * @param {Object} params Parameters object + * @param {object} params Parameters object * @param {string} params.accessToken valid access token * @param {object} params.cliCommandFlags cli flags * @param {object} params.project project details * @param {string} [params.env='prod'] valid env stage|prod + * @returns {Promise} Promise that resolves when the audit log is sent successfully */ async function sendAppAssetsUndeployedAuditLog ({ accessToken, cliCommandFlags, project, env }) { const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_ASSETS_UNDEPLOYED) - return sendAuditLogs(accessToken, logEvent, env) + return sendAuditLogs({ accessToken, logEvent, env }) } /** + * Creates an audit log event object * * @param {object} cliCommandFlags cli flags * @param {object} project details * @param {string} operation one of: ab_app_deploy, ab_app_undeploy, ab_app_assets_deployed, ab_app_assets_undeployed - * @returns {object} logEvent + * @returns {object} logEvent object containing audit log details + * @throws {Error} if project, project.org, or project.workspace is missing, or if operation is invalid */ function getAuditLogEvent (cliCommandFlags, project, operation) { if (!project) { @@ -169,9 +174,8 @@ function getAuditLogEvent (cliCommandFlags, project, operation) { module.exports = { OPERATIONS, - sendAuditLogs, - getAuditLogEvent, AUDIT_SERVICE_ENDPOINTS, + getAuditLogEvent, sendAppDeployAuditLog, sendAppUndeployAuditLog, sendAppAssetsDeployedAuditLog, diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index b29b3765e..3e19f4f4e 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -171,21 +171,6 @@ beforeEach(() => { helpers.wrapError.mockImplementation(msg => msg) helpers.createWebExportFilter.mockImplementation(filterValue => helpersActual.createWebExportFilter(filterValue)) - auditLogger.getAuditLogEvent.mockImplementation((flags, project, event) => { - return { - orgId: 'mockorg', - projectId: 'mockproject', - workspaceId: 'mockworkspaceid', - workspaceName: 'mockworkspacename', - operation: 'AB_APP_ASSETS_DEPLOYED'.toLowerCase(), - timestamp: new Date().valueOf(), - data: { - cliCommandFlags: flags, - opDetailsStr: 'logStrMsg', - opItems: [] - } - } - }) helpers.getFilesCountWithExtension.mockImplementation((dir) => { return [ '3 Javascript file(s)', @@ -1309,7 +1294,7 @@ describe('run', () => { accessToken: mockToken, env: mockEnv }) - command.getFullConfig = jest.fn().mockReturnValue({ + const fullConfig = { aio: { project: { id: mockProject, @@ -1322,16 +1307,25 @@ describe('run', () => { } } } - }) + } + command.getFullConfig = jest.fn().mockReturnValue(fullConfig) command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) await command.run() expect(command.error).toHaveBeenCalledTimes(0) expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) - expect(auditLogger.sendAuditLogs).toHaveBeenCalledTimes(1) + expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(1) expect(authHelper.setRuntimeApiHostAndAuthHandler).toHaveBeenCalledTimes(1) - expect(auditLogger.sendAuditLogs).toHaveBeenCalledWith(mockToken, expect.objectContaining({ orgId: mockOrg, projectId: mockProject, workspaceId: mockWorkspaceId, workspaceName: mockWorkspaceName }), mockEnv) + expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledWith( + { + accessToken: mockToken, + cliCommandFlags: expect.any(Object), + opItems: expect.any(Array), + project: fullConfig.aio.project, + env: mockEnv + } + ) process.env.IS_DEPLOY_SERVICE_ENABLED = false }) @@ -1346,42 +1340,8 @@ describe('run', () => { accessToken: mockToken, env: mockEnv }) - command.getFullConfig = jest.fn().mockReturnValue({ - aio: { - project: { - id: mockProject, - org: { - id: mockOrg - }, - workspace: { - id: mockWorkspaceId, - name: mockWorkspaceName - } - } - } - }) - command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) - - await command.run() - expect(command.error).toHaveBeenCalledTimes(0) - expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) - expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) - expect(auditLogger.sendAuditLogs).toHaveBeenCalledTimes(1) - expect(auditLogger.sendAuditLogs).toHaveBeenCalledWith(mockToken, expect.objectContaining({ orgId: mockOrg, projectId: mockProject, workspaceId: mockWorkspaceId, workspaceName: mockWorkspaceName }), mockEnv) - }) - test('Do not send audit logs for successful app deploy, if no logevent is present', async () => { - const mockToken = 'mocktoken' - const mockEnv = 'stage' - const mockOrg = 'mockorg' - const mockProject = 'mockproject' - const mockWorkspaceId = 'mockworkspaceid' - const mockWorkspaceName = 'mockworkspacename' - helpers.getCliInfo.mockResolvedValueOnce({ - accessToken: mockToken, - env: mockEnv - }) - command.getFullConfig = jest.fn().mockReturnValue({ + const fullConfig = { aio: { project: { id: mockProject, @@ -1394,17 +1354,24 @@ describe('run', () => { } } } - }) - - auditLogger.getAuditLogEvent.mockImplementation((flags, project, event) => null) - + } + command.getFullConfig = jest.fn().mockReturnValue(fullConfig) command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) await command.run() expect(command.error).toHaveBeenCalledTimes(0) expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) - expect(auditLogger.sendAuditLogs).toHaveBeenCalledTimes(0) + expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(1) + expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledWith( + { + accessToken: mockToken, + cliCommandFlags: expect.any(Object), + opItems: expect.any(Array), + project: fullConfig.aio.project, + env: mockEnv + } + ) }) test('Do not send audit logs for successful app deploy, if case of no token', async () => { @@ -1420,7 +1387,7 @@ describe('run', () => { env: mockEnv }) - command.getFullConfig = jest.fn().mockReturnValue({ + const fullConfig = { aio: { project: { id: mockProject, @@ -1433,14 +1400,15 @@ describe('run', () => { } } } - }) + } + command.getFullConfig = jest.fn().mockReturnValue(fullConfig) command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) await command.run() expect(command.error).toHaveBeenCalledTimes(0) expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) - expect(auditLogger.sendAuditLogs).toHaveBeenCalledTimes(0) + expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(0) }) test('Send audit logs for successful app deploy + web assets', async () => { @@ -1458,7 +1426,7 @@ describe('run', () => { env: mockEnv }) - command.getFullConfig = jest.fn().mockReturnValue({ + const fullConfig = { aio: { project: { id: mockProject, @@ -1471,8 +1439,8 @@ describe('run', () => { } } } - }) - + } + command.getFullConfig = jest.fn().mockReturnValue(fullConfig) command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) await command.run() @@ -1480,7 +1448,15 @@ describe('run', () => { expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) expect(helpers.getFilesCountWithExtension).toHaveBeenCalledTimes(2) - expect(auditLogger.sendAuditLogs).toHaveBeenCalledWith(mockToken, expect.objectContaining({ orgId: mockOrg, projectId: mockProject, workspaceId: mockWorkspaceId, workspaceName: mockWorkspaceName }), mockEnv) + expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledWith( + { + accessToken: mockToken, + cliCommandFlags: expect.any(Object), + opItems: expect.any(Array), + project: fullConfig.aio.project, + env: mockEnv + } + ) }) test('Should deploy successfully even if Audit log service is unavailable', async () => { @@ -1509,7 +1485,7 @@ describe('run', () => { } }) - auditLogger.sendAuditLogs.mockRejectedValue({ + auditLogger.sendAppAssetsDeployedAuditLog.mockRejectedValue({ message: 'Internal Server Error', status: 500 }) @@ -1524,7 +1500,7 @@ describe('run', () => { expect(command.log).toHaveBeenCalledWith( expect.stringContaining('Successful deployment 🏄') ) - expect(auditLogger.sendAuditLogs).toHaveBeenCalledTimes(1) + expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(1) expect(command.error).toHaveBeenCalledTimes(0) expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) @@ -1556,7 +1532,7 @@ describe('run', () => { } }) - auditLogger.sendAuditLogs.mockRejectedValue({ + auditLogger.sendAppAssetsDeployedAuditLog.mockRejectedValue({ message: 'Internal Server Error', status: 500 }) @@ -1572,7 +1548,7 @@ describe('run', () => { expect(command.log).toHaveBeenCalledWith( expect.stringContaining('Successful deployment 🏄') ) - expect(auditLogger.sendAuditLogs).toHaveBeenCalledTimes(1) + expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(1) expect(command.error).toHaveBeenCalledTimes(0) expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) diff --git a/test/commands/app/undeploy.test.js b/test/commands/app/undeploy.test.js index fd96e91aa..77e4bb5f0 100644 --- a/test/commands/app/undeploy.test.js +++ b/test/commands/app/undeploy.test.js @@ -516,7 +516,7 @@ describe('run', () => { const mockWorkspaceId = 'mockworkspaceid' const mockWorkspaceName = 'mockworkspacename' - command.getFullConfig = jest.fn().mockReturnValue({ + const fullConfig = { aio: { project: { id: mockProject, @@ -529,15 +529,23 @@ describe('run', () => { } } } - }) + } + command.getFullConfig = jest.fn().mockReturnValue(fullConfig) command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) await command.run() expect(command.error).toHaveBeenCalledTimes(0) expect(mockRuntimeLib.undeployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(1) - expect(auditLogger.sendAuditLogs.mock.calls.length).toBe(1) - expect(auditLogger.sendAuditLogs).toHaveBeenCalledWith(mockToken, expect.objectContaining({ orgId: mockOrg, projectId: mockProject, workspaceId: mockWorkspaceId, workspaceName: mockWorkspaceName }), mockEnv) + expect(auditLogger.sendAppAssetsUndeployedAuditLog.mock.calls.length).toBe(1) + expect(auditLogger.sendAppAssetsUndeployedAuditLog).toHaveBeenCalledWith( + { + accessToken: mockToken, + cliCommandFlags: expect.any(Object), + project: fullConfig.aio.project, + env: mockEnv + } + ) }) test('Do not Send audit logs for successful app undeploy if case of no-token', async () => { @@ -567,38 +575,7 @@ describe('run', () => { expect(command.error).toHaveBeenCalledTimes(0) expect(mockRuntimeLib.undeployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(1) - expect(auditLogger.sendAuditLogs.mock.calls.length).toBe(0) - }) - - test('Do not Send audit logs for successful app undeploy, if no logevent is present', async () => { - const mockOrg = 'mockorg' - const mockProject = 'mockproject' - const mockWorkspaceId = 'mockworkspaceid' - const mockWorkspaceName = 'mockworkspacename' - - command.getFullConfig = jest.fn().mockReturnValue({ - aio: { - project: { - id: mockProject, - org: { - id: mockOrg - }, - workspace: { - id: mockWorkspaceId, - name: mockWorkspaceName - } - } - } - }) - command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) - - auditLogger.getAuditLogEvent.mockImplementation((flags, project, event) => null) - - await command.run() - expect(command.error).toHaveBeenCalledTimes(0) - expect(mockRuntimeLib.undeployActions).toHaveBeenCalledTimes(1) - expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(1) - expect(auditLogger.sendAuditLogs.mock.calls.length).toBe(0) + expect(auditLogger.sendAppAssetsUndeployedAuditLog.mock.calls.length).toBe(0) }) test('Should app undeploy successfully even if Audit Log Service is not available', async () => { @@ -623,7 +600,7 @@ describe('run', () => { }) command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) - auditLogger.sendAuditLogs.mockRejectedValue({ + auditLogger.sendAppAssetsUndeployedAuditLog.mockRejectedValue({ message: 'Internal Server Error', status: 500 }) @@ -632,6 +609,6 @@ describe('run', () => { expect(command.error).toHaveBeenCalledTimes(0) expect(mockRuntimeLib.undeployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(1) - expect(auditLogger.sendAuditLogs).toHaveBeenCalledTimes(1) + expect(auditLogger.sendAppAssetsUndeployedAuditLog).toHaveBeenCalledTimes(1) }) }) diff --git a/test/commands/lib/app-helper.test.js b/test/commands/lib/app-helper.test.js index 1ddd97e1c..f7fd53ec6 100644 --- a/test/commands/lib/app-helper.test.js +++ b/test/commands/lib/app-helper.test.js @@ -815,7 +815,7 @@ describe('getFilesCountWithExtension', () => { it('should return an error message when directory does not exist', () => { fs.existsSync.mockReturnValue(false) - + expect(() => appHelper.getFilesCountWithExtension(directory)).toThrow(`Error: Directory ${directory} does not exist.`) expect(fs.existsSync).toHaveBeenCalledWith(directory) }) diff --git a/test/commands/lib/audit-logger.test.js b/test/commands/lib/audit-logger.test.js index 036388c95..6c4dfd9a9 100644 --- a/test/commands/lib/audit-logger.test.js +++ b/test/commands/lib/audit-logger.test.js @@ -3,207 +3,206 @@ Copyright 2024 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -const fs = require('node:fs') -const path = require('node:path') -const chalk = require('chalk') const fetch = require('node-fetch') -jest.mock('node-fetch', () => jest.fn()) -const auditLogger = require('../../../src/lib/audit-logger') -const { getCliEnv } = require('@adobe/aio-lib-env') - -jest.mock('fs') -jest.mock('chalk', () => ({ - red: jest.fn((text) => text), - bold: jest.fn((text) => text) -})) - -const mockToken = 'mocktoken' -const mockEnv = 'stage' -const mockLogEvent = { - projectId: 'mockproject', - orgId: 'mockorg' -} - -const mockResponse = Promise.resolve({ - ok: true, - status: 200, - text: () => { - return {} - } -}) - -const mockErrorResponse = Promise.resolve({ - ok: false, - status: 400, - text: () => { - return {} - } -}) - -beforeEach(() => { - fetch.mockReset() -}) - -test('sendAuditLogs with valid params', async () => { - fetch.mockReturnValue(mockResponse) - const options = { - method: 'POST', - headers: { - Authorization: 'Bearer ' + mockToken, - 'Content-type': 'application/json' - }, - body: JSON.stringify({ event: mockLogEvent }) +const { + OPERATIONS, + AUDIT_SERVICE_ENDPOINTS, + getAuditLogEvent, + sendAppDeployAuditLog, + sendAppUndeployAuditLog, + sendAppAssetsDeployedAuditLog, + sendAppAssetsUndeployedAuditLog +} = require('../../../src/lib/audit-logger') + +jest.mock('node-fetch') + +describe('audit-logger', () => { + const mockAccessToken = 'fake-token' + const mockProject = { + org: { id: 'fake-org-id' }, + id: 'fake-project-id', + workspace: { + id: 'fake-workspace-id', + name: 'fake-workspace' + } } - await auditLogger.sendAuditLogs(mockToken, mockLogEvent, mockEnv) - expect(fetch).toHaveBeenCalledTimes(1) - expect(fetch).toHaveBeenCalledWith(auditLogger.AUDIT_SERVICE_ENDPOINTS[mockEnv], options) -}) + const mockCliFlags = { flag1: 'value1' } -// NOTE: this test is blocked until the audit service is available in prod -test('sendAuditLogs with default params', async () => { - fetch.mockReturnValue(mockResponse) - const options = { - method: 'POST', - headers: { - Authorization: 'Bearer ' + mockToken, - 'Content-type': 'application/json' - }, - body: JSON.stringify({ event: mockLogEvent }) - } - await auditLogger.sendAuditLogs(mockToken, mockLogEvent) - expect(fetch).toHaveBeenCalledTimes(1) - expect(fetch).toHaveBeenCalledWith(auditLogger.AUDIT_SERVICE_ENDPOINTS.prod, options) -}) + beforeEach(() => { + jest.clearAllMocks() + }) -test('should take prod endpoint if calling sendAuditLogs with non-exisiting env', async () => { - fetch.mockReturnValue(mockResponse) - const options = { - method: 'POST', - headers: { - Authorization: 'Bearer ' + mockToken, - 'Content-type': 'application/json' - }, - body: JSON.stringify({ event: mockLogEvent }) - } - await auditLogger.sendAuditLogs(mockToken, mockLogEvent, 'dev') - expect(fetch).toHaveBeenCalledTimes(1) - expect(fetch).toHaveBeenCalledWith(auditLogger.AUDIT_SERVICE_ENDPOINTS.prod, options) -}) + describe('getAuditLogEvent', () => { + it('should create a valid audit log event for app deploy', () => { + const event = getAuditLogEvent(mockCliFlags, mockProject, OPERATIONS.AB_APP_DEPLOY) + + expect(event).toEqual({ + orgId: 'fake-org-id', + projectId: 'fake-project-id', + workspaceId: 'fake-workspace-id', + workspaceName: 'fake-workspace', + operation: OPERATIONS.AB_APP_DEPLOY, + timestamp: expect.any(Number), + data: { + cliCommandFlags: mockCliFlags, + opDetailsStr: expect.stringContaining('Starting deployment for the App Builder application') + } + }) + }) -test('sendAuditLogs error response', async () => { - fetch.mockReturnValue(mockErrorResponse) - const options = { - method: 'POST', - headers: { - Authorization: 'Bearer ' + mockToken, - 'Content-type': 'application/json' - }, - body: JSON.stringify({ event: mockLogEvent }) - } - await expect(auditLogger.sendAuditLogs(mockToken, mockLogEvent, mockEnv)).rejects.toThrow('Failed to send audit log - 400') - expect(fetch).toHaveBeenCalledTimes(1) - expect(fetch).toHaveBeenCalledWith(auditLogger.AUDIT_SERVICE_ENDPOINTS[mockEnv], options) -}) + it('should create a valid audit log event for app undeploy', () => { + const event = getAuditLogEvent(mockCliFlags, mockProject, OPERATIONS.AB_APP_UNDEPLOY) + + expect(event).toEqual({ + orgId: 'fake-org-id', + projectId: 'fake-project-id', + workspaceId: 'fake-workspace-id', + workspaceName: 'fake-workspace', + operation: OPERATIONS.AB_APP_UNDEPLOY, + timestamp: expect.any(Number), + data: { + cliCommandFlags: mockCliFlags, + opDetailsStr: expect.stringContaining('Starting undeployment for the App Builder application') + } + }) + }) -describe('getAuditLogEvent', () => { - const cliCommandFlags = { flag1: 'value1' } - const project = { - org: { id: 'org123' }, - id: 'proj456', - workspace: { id: 'ws789', name: 'testWorkspace' } - } + it('should throw error if project is missing', () => { + expect(() => getAuditLogEvent(mockCliFlags, null, OPERATIONS.AB_APP_DEPLOY)) + .toThrow('Project is required') + }) - const mockDeployMessage = 'Starting deployment for the App Builder application in workspace testWorkspace' - const mockUndeployMessage = 'Starting undeployment for the App Builder application in workspace testWorkspace' - - test('should return correct log event for AB_APP_DEPLOY event', () => { - const result = auditLogger.getAuditLogEvent(cliCommandFlags, project, auditLogger.OPERATIONS.AB_APP_DEPLOY) - - expect(result).toEqual({ - orgId: 'org123', - projectId: 'proj456', - workspaceId: 'ws789', - workspaceName: 'testWorkspace', - operation: auditLogger.OPERATIONS.AB_APP_DEPLOY, - timestamp: expect.any(Number), - data: { - cliCommandFlags, - opDetailsStr: mockDeployMessage - } + it('should throw error if project org is missing', () => { + const invalidProject = { ...mockProject, org: null } + expect(() => getAuditLogEvent(mockCliFlags, invalidProject, OPERATIONS.AB_APP_DEPLOY)) + .toThrow('Project org is required') }) - }) - test('should return correct log event for AB_APP_UNDEPLOY event', () => { - const result = auditLogger.getAuditLogEvent(cliCommandFlags, project, auditLogger.OPERATIONS.AB_APP_UNDEPLOY) - - expect(result).toEqual({ - orgId: 'org123', - projectId: 'proj456', - workspaceId: 'ws789', - workspaceName: 'testWorkspace', - operation: auditLogger.OPERATIONS.AB_APP_UNDEPLOY, - timestamp: expect.any(Number), - data: { - cliCommandFlags, - opDetailsStr: mockUndeployMessage - } + it('should throw error if project workspace is missing', () => { + const invalidProject = { ...mockProject, workspace: null } + expect(() => getAuditLogEvent(mockCliFlags, invalidProject, OPERATIONS.AB_APP_DEPLOY)) + .toThrow('Project workspace is required') }) - }) - test('should return correct log event for AB_APP_ASSETS_UNDEPLOYED event', () => { - const result = auditLogger.getAuditLogEvent(cliCommandFlags, project, auditLogger.OPERATIONS.AB_APP_ASSETS_UNDEPLOYED) - - expect(result).toEqual({ - orgId: 'org123', - projectId: 'proj456', - workspaceId: 'ws789', - workspaceName: 'testWorkspace', - operation: auditLogger.OPERATIONS.AB_APP_ASSETS_UNDEPLOYED, - timestamp: expect.any(Number), - data: { - cliCommandFlags, - opDetailsStr: 'All static assets for the App Builder application in workspace: testWorkspace were successfully undeployed from the CDN' - } + it('should throw error for invalid operation', () => { + expect(() => getAuditLogEvent(mockCliFlags, mockProject, 'invalid_operation')) + .toThrow('Invalid operation: invalid_operation') }) }) - test('should return correct log event for AB_APP_ASSETS_DEPLOYED event', () => { - const result = auditLogger.getAuditLogEvent(cliCommandFlags, project, auditLogger.OPERATIONS.AB_APP_ASSETS_DEPLOYED) - - expect(result).toEqual({ - orgId: 'org123', - projectId: 'proj456', - workspaceId: 'ws789', - workspaceName: 'testWorkspace', - operation: auditLogger.OPERATIONS.AB_APP_ASSETS_DEPLOYED, - timestamp: expect.any(Number), - data: { - cliCommandFlags, - opDetailsStr: 'All static assets for the App Builder application in workspace: testWorkspace were successfully deployed to the CDN.\n Files deployed - ' - } + describe('sendAppDeployAuditLog', () => { + it('should send app deploy audit log successfully', async () => { + fetch.mockResolvedValueOnce({ status: 200 }) + + await sendAppDeployAuditLog({ + accessToken: mockAccessToken, + cliCommandFlags: mockCliFlags, + project: mockProject + }) + + expect(fetch).toHaveBeenCalledWith( + AUDIT_SERVICE_ENDPOINTS.prod, + expect.objectContaining({ + method: 'POST', + headers: { + Authorization: `Bearer ${mockAccessToken}`, + 'Content-type': 'application/json' + } + }) + ) }) }) - test('should throw error if project is missing', () => { - expect(() => auditLogger.getAuditLogEvent(cliCommandFlags, null, auditLogger.OPERATIONS.AB_APP_DEPLOY)).toThrow('Project is required') + describe('sendAppUndeployAuditLog', () => { + it('should send app undeploy audit log successfully', async () => { + fetch.mockResolvedValueOnce({ status: 200 }) + + await sendAppUndeployAuditLog({ + accessToken: mockAccessToken, + cliCommandFlags: mockCliFlags, + project: mockProject + }) + + expect(fetch).toHaveBeenCalledWith( + AUDIT_SERVICE_ENDPOINTS.prod, + expect.objectContaining({ + method: 'POST', + headers: { + Authorization: `Bearer ${mockAccessToken}`, + 'Content-type': 'application/json' + } + }) + ) + }) }) - test('should throw error if project org is missing', () => { - expect(() => auditLogger.getAuditLogEvent(cliCommandFlags, {}, auditLogger.OPERATIONS.AB_APP_DEPLOY)).toThrow('Project org is required') + describe('sendAppAssetsDeployedAuditLog', () => { + it('should send app assets deployed audit log successfully', async () => { + fetch.mockResolvedValueOnce({ status: 200 }) + const mockOpItems = ['file1.js', 'file2.css'] + + await sendAppAssetsDeployedAuditLog({ + accessToken: mockAccessToken, + cliCommandFlags: mockCliFlags, + project: mockProject, + opItems: mockOpItems + }) + + expect(fetch).toHaveBeenCalledWith( + AUDIT_SERVICE_ENDPOINTS.prod, + expect.objectContaining({ + method: 'POST', + headers: { + Authorization: `Bearer ${mockAccessToken}`, + 'Content-type': 'application/json' + }, + body: expect.stringContaining(JSON.stringify(mockOpItems)) + }) + ) + }) }) - test('should throw error if project workspace is missing', () => { - expect(() => auditLogger.getAuditLogEvent(cliCommandFlags, { org: { id: 'org123' } }, auditLogger.OPERATIONS.AB_APP_DEPLOY)).toThrow('Project workspace is required') + describe('sendAppAssetsUndeployedAuditLog', () => { + it('should send app assets undeployed audit log successfully', async () => { + fetch.mockResolvedValueOnce({ status: 200 }) + + await sendAppAssetsUndeployedAuditLog({ + accessToken: mockAccessToken, + cliCommandFlags: mockCliFlags, + project: mockProject + }) + + expect(fetch).toHaveBeenCalledWith( + AUDIT_SERVICE_ENDPOINTS.prod, + expect.objectContaining({ + method: 'POST', + headers: { + Authorization: `Bearer ${mockAccessToken}`, + 'Content-type': 'application/json' + } + }) + ) + }) }) - test('should throw error if event is not found in OPERATIONS', () => { - expect(() => auditLogger.getAuditLogEvent(cliCommandFlags, project, 'UNKNOWN_OPERATION')).toThrow('Invalid operation: UNKNOWN_OPERATION') + describe('error handling', () => { + it('should throw error when audit service returns non-200 status', async () => { + fetch.mockResolvedValueOnce({ + status: 500, + text: () => Promise.resolve('Internal Server Error') + }) + + await expect(sendAppDeployAuditLog({ + accessToken: mockAccessToken, + cliCommandFlags: mockCliFlags, + project: mockProject + })).rejects.toThrow('Failed to send audit log - 500 Internal Server Error') + }) }) }) diff --git a/test/jest.setup.js b/test/jest.setup.js index 6c7320a44..5f3c8f27c 100644 --- a/test/jest.setup.js +++ b/test/jest.setup.js @@ -26,7 +26,7 @@ beforeEach(() => { stdout.start() stderr.start() // change this if you need to see logs from stdout - stdout.print = true + stdout.print = false }) afterEach(() => { stdout.stop(); stderr.stop() }) From a40a0cc35af55fb61c05da1bcad03cb530f1ee0c Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Thu, 24 Apr 2025 11:58:55 +0800 Subject: [PATCH 03/21] cleanup --- package.json | 2 +- src/lib/audit-logger.js | 62 ++++++++++++-------------- test/commands/lib/audit-logger.test.js | 22 +++++++++ 3 files changed, 52 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 12e931c7b..0c20e444f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "bugs": "https://github.com/adobe/aio-cli-plugin-app/issues", "dependencies": { "@adobe/aio-cli-lib-app-config": "^4.0.3", - "@adobe/aio-cli-lib-console": "^5", + "@adobe/aio-cli-lib-console": "^5.0.3", "@adobe/aio-lib-core-config": "^5", "@adobe/aio-lib-core-logging": "^3", "@adobe/aio-lib-core-networking": "^5", diff --git a/src/lib/audit-logger.js b/src/lib/audit-logger.js index 767485032..83cb747cf 100644 --- a/src/lib/audit-logger.js +++ b/src/lib/audit-logger.js @@ -23,15 +23,28 @@ const AUDIT_SERVICE_ENDPOINTS = { } /** - * Send audit log events to audit service + * @typedef {object} AuditLogParams + * @property {string} accessToken - valid access token + * @property {object} cliCommandFlags - cli flags + * @property {object} project - project details + * @property {Array} [opItems] - list of deployed files (only for assets deployment) + * @property {string} [env='prod'] - valid env stage|prod + */ + +/** + * @typedef {object} PublishAuditLogParams + * @property {string} accessToken - valid access token + * @property {object} logEvent - logEvent details + * @property {string} [env='prod'] - valid env stage|prod + */ + +/** + * Publish audit log events to audit service * - * @param {object} params Parameters object - * @param {string} params.accessToken valid access token - * @param {object} params.logEvent logEvent details - * @param {string} [params.env='prod'] valid env stage|prod + * @param {PublishAuditLogParams} params Parameters object * @returns {Promise} Promise that resolves when the audit log is sent successfully */ -async function sendAuditLogs ({ accessToken, logEvent, env = 'prod' }) { +async function publishAuditLogs ({ accessToken, logEvent, env = 'prod' }) { const url = AUDIT_SERVICE_ENDPOINTS[env] ?? AUDIT_SERVICE_ENDPOINTS.prod const payload = { event: logEvent @@ -39,7 +52,7 @@ async function sendAuditLogs ({ accessToken, logEvent, env = 'prod' }) { const options = { method: 'POST', headers: { - Authorization: 'Bearer ' + accessToken, + Authorization: `Bearer ${accessToken}`, 'Content-type': 'application/json' }, body: JSON.stringify(payload) @@ -47,70 +60,53 @@ async function sendAuditLogs ({ accessToken, logEvent, env = 'prod' }) { const response = await fetch(url, options) if (response.status !== 200) { const err = await response.text() - throw new Error('Failed to send audit log - ' + response.status + ' ' + err) + throw new Error(`Failed to send audit log - ${response.status} ${err}`) } } /** * Send audit log event for app deployment * - * @param {object} params Parameters object - * @param {string} params.accessToken valid access token - * @param {object} params.cliCommandFlags cli flags - * @param {object} params.project project details - * @param {string} [params.env='prod'] valid env stage|prod + * @param {AuditLogParams} params Parameters object * @returns {Promise} Promise that resolves when the audit log is sent successfully */ async function sendAppDeployAuditLog ({ accessToken, cliCommandFlags, project, env }) { const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_DEPLOY) - return sendAuditLogs({ accessToken, logEvent, env }) + return publishAuditLogs({ accessToken, logEvent, env }) } /** * Send audit log event for app undeployment * - * @param {object} params Parameters object - * @param {string} params.accessToken valid access token - * @param {object} params.cliCommandFlags cli flags - * @param {object} params.project project details - * @param {string} [params.env='prod'] valid env stage|prod + * @param {AuditLogParams} params Parameters object * @returns {Promise} Promise that resolves when the audit log is sent successfully */ async function sendAppUndeployAuditLog ({ accessToken, cliCommandFlags, project, env }) { const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_UNDEPLOY) - return sendAuditLogs({ accessToken, logEvent, env }) + return publishAuditLogs({ accessToken, logEvent, env }) } /** * Send audit log event for app assets deployment * - * @param {object} params Parameters object - * @param {string} params.accessToken valid access token - * @param {object} params.cliCommandFlags cli flags - * @param {object} params.project project details - * @param {Array} params.opItems list of deployed files - * @param {string} [params.env='prod'] valid env stage|prod + * @param {AuditLogParams} params Parameters object * @returns {Promise} Promise that resolves when the audit log is sent successfully */ async function sendAppAssetsDeployedAuditLog ({ accessToken, cliCommandFlags, project, opItems, env }) { const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_ASSETS_DEPLOYED) logEvent.data.opItems = opItems - return sendAuditLogs({ accessToken, logEvent, env }) + return publishAuditLogs({ accessToken, logEvent, env }) } /** * Send audit log event for app assets undeployment * - * @param {object} params Parameters object - * @param {string} params.accessToken valid access token - * @param {object} params.cliCommandFlags cli flags - * @param {object} params.project project details - * @param {string} [params.env='prod'] valid env stage|prod + * @param {AuditLogParams} params Parameters object * @returns {Promise} Promise that resolves when the audit log is sent successfully */ async function sendAppAssetsUndeployedAuditLog ({ accessToken, cliCommandFlags, project, env }) { const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_ASSETS_UNDEPLOYED) - return sendAuditLogs({ accessToken, logEvent, env }) + return publishAuditLogs({ accessToken, logEvent, env }) } /** diff --git a/test/commands/lib/audit-logger.test.js b/test/commands/lib/audit-logger.test.js index 6c4dfd9a9..62fa191fa 100644 --- a/test/commands/lib/audit-logger.test.js +++ b/test/commands/lib/audit-logger.test.js @@ -204,5 +204,27 @@ describe('audit-logger', () => { project: mockProject })).rejects.toThrow('Failed to send audit log - 500 Internal Server Error') }) + + it('should use prod endpoint when invalid environment is provided', async () => { + fetch.mockResolvedValueOnce({ status: 200 }) + + await sendAppDeployAuditLog({ + accessToken: mockAccessToken, + cliCommandFlags: mockCliFlags, + project: mockProject, + env: 'invalid-env' + }) + + expect(fetch).toHaveBeenCalledWith( + AUDIT_SERVICE_ENDPOINTS.prod, + expect.objectContaining({ + method: 'POST', + headers: { + Authorization: `Bearer ${mockAccessToken}`, + 'Content-type': 'application/json' + } + }) + ) + }) }) }) From e6e7fd628ba82f42958788f6741e40c458c21114 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Fri, 25 Apr 2025 08:19:30 +0800 Subject: [PATCH 04/21] start support for appName, appVersion, objectName in audit-log --- src/lib/audit-logger.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/audit-logger.js b/src/lib/audit-logger.js index 83cb747cf..a840e052e 100644 --- a/src/lib/audit-logger.js +++ b/src/lib/audit-logger.js @@ -130,6 +130,8 @@ function getAuditLogEvent (cliCommandFlags, project, operation) { } const workspaceName = project.workspace.name + // TODO: get app name, version, and object name + let appName, appVersion, objectName let logStrMsg switch (operation) { @@ -159,6 +161,9 @@ function getAuditLogEvent (cliCommandFlags, project, operation) { workspaceId, workspaceName, operation, + appName, + appVersion, + objectName, timestamp: new Date().valueOf(), data: { cliCommandFlags, From 4528dee27d01d9abdf3827f49ee7e1159a996be4 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Wed, 30 Apr 2025 17:43:21 +0800 Subject: [PATCH 05/21] add audit log calls (tests pending) --- src/commands/app/deploy.js | 53 ++++++++---- src/commands/app/undeploy.js | 45 +++++++--- src/lib/app-helper.js | 25 ++++++ src/lib/audit-logger.js | 157 ++++++++++++++++++++--------------- 4 files changed, 181 insertions(+), 99 deletions(-) diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index 098143f06..852dc8bb3 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -21,7 +21,7 @@ const { Flags } = require('@oclif/core') const { runInProcess, buildExtensionPointPayloadWoMetadata, buildExcShellViewExtensionMetadata, getCliInfo, getFilesCountWithExtension } = require('../../lib/app-helper') const rtLib = require('@adobe/aio-lib-runtime') const LogForwarding = require('../../lib/log-forwarding') -const { sendAppAssetsDeployedAuditLog } = require('../../lib/audit-logger') +const { sendAppAssetsDeployedAuditLog, sendAppDeployAuditLog } = require('../../lib/audit-logger') const { setRuntimeApiHostAndAuthHandler } = require('../../lib/auth-helper') const logActions = require('../../lib/log-actions') @@ -53,8 +53,29 @@ class Deploy extends BuildCommand { const spinner = ora() try { - const aioConfig = (await this.getFullConfig()).aio + const { aio: aioConfig, packagejson: packageJson } = await this.getFullConfig() const cliDetails = await getCliInfo(flags.publish) + const appInfo = { + name: packageJson.name, + version: packageJson.version, + project: aioConfig?.project, + runtimeNamespace: aioConfig?.runtime?.namespace + } + + try { + // send audit log at start (don't wait for deployment to finish) + await sendAppDeployAuditLog({ + accessToken: cliDetails?.accessToken, + cliCommandFlags: flags, + appInfo, + env: cliDetails.env + }) + } catch (error) { + if (flags.verbose) { + this.warn('Error: Audit Log Service Error: Failed to send audit log event for deployment.') + this.warn(error.message) + } + } // 1. update log forwarding configuration // note: it is possible that .aio file does not exist, which means there is no local lg config @@ -106,21 +127,19 @@ class Deploy extends BuildCommand { await this.deploySingleConfig(k, v, flags, spinner) if (v.app.hasFrontend && flags['web-assets']) { const opItems = getFilesCountWithExtension(v.web.distProd) - if (cliDetails?.accessToken) { - try { - // only send logs in case of web-assets deployment - await sendAppAssetsDeployedAuditLog({ - accessToken: cliDetails.accessToken, - cliCommandFlags: flags, - project: aioConfig.project, - opItems, - env: cliDetails.env - }) - } catch (error) { - if (flags.verbose) { - this.warn('Error: Audit Log Service Error: Failed to send audit log event for deployment.') - this.warn(error.message) - } + try { + // only send logs in case of web-assets deployment + await sendAppAssetsDeployedAuditLog({ + accessToken: cliDetails?.accessToken, + cliCommandFlags: flags, + opItems, + appInfo, + env: cliDetails.env + }) + } catch (error) { + if (flags.verbose) { + this.warn('Error: Audit Log Service Error: Failed to send audit log event for deployment.') + this.warn(error.message) } } } diff --git a/src/commands/app/undeploy.js b/src/commands/app/undeploy.js index 1cb8eb0c2..72b55e686 100644 --- a/src/commands/app/undeploy.js +++ b/src/commands/app/undeploy.js @@ -19,7 +19,7 @@ const BaseCommand = require('../../BaseCommand') const webLib = require('@adobe/aio-lib-web') const { runInProcess, buildExtensionPointPayloadWoMetadata, getCliInfo } = require('../../lib/app-helper') const rtLib = require('@adobe/aio-lib-runtime') -const { sendAppAssetsUndeployedAuditLog } = require('../../lib/audit-logger') +const { sendAppAssetsUndeployedAuditLog, sendAppUndeployAuditLog } = require('../../lib/audit-logger') const { setRuntimeApiHostAndAuthHandler } = require('../../lib/auth-helper') class Undeploy extends BaseCommand { @@ -51,8 +51,29 @@ class Undeploy extends BaseCommand { const spinner = ora() try { - const aioConfig = (await this.getFullConfig()).aio + const { aio: aioConfig, packagejson: packageJson } = await this.getFullConfig() const cliDetails = await getCliInfo(flags.unpublish) + const appInfo = { + name: packageJson.name, + version: packageJson.version, + project: aioConfig?.project, + runtimeNamespace: aioConfig?.runtime?.namespace + } + + try { + // send audit log at start (don't wait for deployment to finish) + await sendAppUndeployAuditLog({ + accessToken: cliDetails?.accessToken, + cliCommandFlags: flags, + appInfo, + env: cliDetails.env + }) + } catch (error) { + if (flags.verbose) { + this.warn('Error: Audit Log Service Error: Failed to send audit log event for deployment.') + this.warn(error.message) + } + } for (let i = 0; i < keys.length; ++i) { const k = keys[i] @@ -61,17 +82,15 @@ class Undeploy extends BaseCommand { await this.undeployOneExt(k, v, flags, spinner) // send logs for case of web-assets undeployment - if (cliDetails?.accessToken) { - try { - await sendAppAssetsUndeployedAuditLog({ - accessToken: cliDetails.accessToken, - cliCommandFlags: flags, - project: aioConfig.project, - env: cliDetails.env - }) - } catch (error) { - this.warn('Warning: Audit Log Service Error: Failed to send audit log event for un-deployment.') - } + try { + await sendAppAssetsUndeployedAuditLog({ + accessToken: cliDetails?.accessToken, + cliCommandFlags: flags, + appInfo, + env: cliDetails.env + }) + } catch (error) { + this.warn('Warning: Audit Log Service Error: Failed to send audit log event for un-deployment.') } } diff --git a/src/lib/app-helper.js b/src/lib/app-helper.js index 4b236e900..9b170e33b 100644 --- a/src/lib/app-helper.js +++ b/src/lib/app-helper.js @@ -534,7 +534,32 @@ function getFilesCountWithExtension (directory) { return log } +/** + * Parses the namespace string into the appropriate components: + * (development, orgId, appName, workspace) + * + * @param {string} namespace the namespace + * @returns {object | null} the path components, or null if no match + */ +function parseNamespaceString (namespace) { + const namespaceLC = namespace.toLowerCase() + const regex = /^(development-)?(\d+)-([a-zA-Z0-9]+)(-([a-zA-Z0-9]+))?$/ + + const match = namespaceLC.match(regex) + if (match) { + return { + development: !!match[1], + orgId: match[2], + appName: match[3], + workspace: match[5] // if empty, it's basically prod + } + } + + return null +} + module.exports = { + parseNamespaceString, getObjectValue, getObjectProp, createWebExportFilter, diff --git a/src/lib/audit-logger.js b/src/lib/audit-logger.js index a840e052e..864461d20 100644 --- a/src/lib/audit-logger.js +++ b/src/lib/audit-logger.js @@ -9,6 +9,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ const fetch = require('node-fetch') +const { parseNamespaceString } = require('./app-helper') const OPERATIONS = { AB_APP_DEPLOY: 'ab_app_deploy', @@ -17,11 +18,16 @@ const OPERATIONS = { AB_APP_ASSETS_UNDEPLOYED: 'ab_app_assets_undeployed' } -const AUDIT_SERVICE_ENDPOINTS = { +const AUDIT_SERVICE_ENDPOINTS2 = { stage: 'https://adp-auditlog-service-stage.adobeioruntime.net/api/v1/web/audit-log-api/event-post', prod: 'https://adp-auditlog-service-prod.adobeioruntime.net/api/v1/web/audit-log-api/event-post' } +const AUDIT_SERVICE_ENDPOINTS = { + stage: 'http://127.0.0.1:3000/audit-log-api/event-post', + prod: 'http://127.0.0.1:3000/audit-log-api/event-post' +} + /** * @typedef {object} AuditLogParams * @property {string} accessToken - valid access token @@ -38,6 +44,14 @@ const AUDIT_SERVICE_ENDPOINTS = { * @property {string} [env='prod'] - valid env stage|prod */ +/** + * @typedef {object} GetAuditLogEventParams + * @property {object} cliCommandFlags - CLI command flags + * @property {object} project - Project details containing org and workspace information + * @property {string} operation - One of: ab_app_deploy, ab_app_undeploy, ab_app_assets_deployed, ab_app_assets_undeployed + * @property {string} [runtimeNamespace] - Optional runtime namespace (for non-logged in use case) + */ + /** * Publish audit log events to audit service * @@ -52,7 +66,7 @@ async function publishAuditLogs ({ accessToken, logEvent, env = 'prod' }) { const options = { method: 'POST', headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: accessToken ? `Bearer ${accessToken}` : '', 'Content-type': 'application/json' }, body: JSON.stringify(payload) @@ -64,74 +78,37 @@ async function publishAuditLogs ({ accessToken, logEvent, env = 'prod' }) { } } -/** - * Send audit log event for app deployment - * - * @param {AuditLogParams} params Parameters object - * @returns {Promise} Promise that resolves when the audit log is sent successfully - */ -async function sendAppDeployAuditLog ({ accessToken, cliCommandFlags, project, env }) { - const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_DEPLOY) - return publishAuditLogs({ accessToken, logEvent, env }) -} - -/** - * Send audit log event for app undeployment - * - * @param {AuditLogParams} params Parameters object - * @returns {Promise} Promise that resolves when the audit log is sent successfully - */ -async function sendAppUndeployAuditLog ({ accessToken, cliCommandFlags, project, env }) { - const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_UNDEPLOY) - return publishAuditLogs({ accessToken, logEvent, env }) -} - -/** - * Send audit log event for app assets deployment - * - * @param {AuditLogParams} params Parameters object - * @returns {Promise} Promise that resolves when the audit log is sent successfully - */ -async function sendAppAssetsDeployedAuditLog ({ accessToken, cliCommandFlags, project, opItems, env }) { - const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_ASSETS_DEPLOYED) - logEvent.data.opItems = opItems - return publishAuditLogs({ accessToken, logEvent, env }) -} - -/** - * Send audit log event for app assets undeployment - * - * @param {AuditLogParams} params Parameters object - * @returns {Promise} Promise that resolves when the audit log is sent successfully - */ -async function sendAppAssetsUndeployedAuditLog ({ accessToken, cliCommandFlags, project, env }) { - const logEvent = getAuditLogEvent(cliCommandFlags, project, OPERATIONS.AB_APP_ASSETS_UNDEPLOYED) - return publishAuditLogs({ accessToken, logEvent, env }) -} - /** * Creates an audit log event object * - * @param {object} cliCommandFlags cli flags - * @param {object} project details - * @param {string} operation one of: ab_app_deploy, ab_app_undeploy, ab_app_assets_deployed, ab_app_assets_undeployed + * @param {GetAuditLogEventParams} params Parameters object * @returns {object} logEvent object containing audit log details * @throws {Error} if project, project.org, or project.workspace is missing, or if operation is invalid */ -function getAuditLogEvent (cliCommandFlags, project, operation) { - if (!project) { - throw new Error('Project is required') - } - if (!project.org) { - throw new Error('Project org is required') - } - if (!project.workspace) { - throw new Error('Project workspace is required') +function getAuditLogEvent ({ cliCommandFlags, operation, appInfo }) { + const { project, runtimeNamespace } = appInfo + + if (!project && !runtimeNamespace) { + throw new Error('Either project or runtimeNamespace is required') } - const workspaceName = project.workspace.name - // TODO: get app name, version, and object name - let appName, appVersion, objectName + let orgId, projectId, workspaceId, workspaceName + + if (project) { // logged in use case + if (!project.org) { + throw new Error('Project org is required') + } + if (!project.workspace) { + throw new Error('Project workspace is required') + } + orgId = project.org.id + projectId = project.id + workspaceId = project.workspace.id + workspaceName = project.workspace.name + } else { // non-logged in use case + const parsedNamespace = parseNamespaceString(runtimeNamespace) + workspaceName = parsedNamespace.workspace ?? 'Production' + } let logStrMsg switch (operation) { @@ -151,19 +128,16 @@ function getAuditLogEvent (cliCommandFlags, project, operation) { throw new Error(`Invalid operation: ${operation}`) } - const orgId = project.org.id - const projectId = project.id - const workspaceId = project.workspace.id - const logEvent = { + runtimeNamespace, orgId, projectId, workspaceId, workspaceName, operation, - appName, - appVersion, - objectName, + appName: appInfo.name, + appVersion: appInfo.version, + objectName: appInfo.name, timestamp: new Date().valueOf(), data: { cliCommandFlags, @@ -173,6 +147,51 @@ function getAuditLogEvent (cliCommandFlags, project, operation) { return logEvent } +/** + * Send audit log event for app deployment + * + * @param {AuditLogParams} params Parameters object + * @returns {Promise} Promise that resolves when the audit log is sent successfully + */ +async function sendAppDeployAuditLog ({ accessToken, cliCommandFlags, appInfo, env }) { + const logEvent = getAuditLogEvent({ cliCommandFlags, appInfo, operation: OPERATIONS.AB_APP_DEPLOY }) + return publishAuditLogs({ accessToken, logEvent, env }) +} + +/** + * Send audit log event for app undeployment + * + * @param {AuditLogParams} params Parameters object + * @returns {Promise} Promise that resolves when the audit log is sent successfully + */ +async function sendAppUndeployAuditLog ({ accessToken, cliCommandFlags, appInfo, env }) { + const logEvent = getAuditLogEvent({ cliCommandFlags, appInfo, operation: OPERATIONS.AB_APP_UNDEPLOY }) + return publishAuditLogs({ accessToken, logEvent, env }) +} + +/** + * Send audit log event for app assets deployment + * + * @param {AuditLogParams} params Parameters object + * @returns {Promise} Promise that resolves when the audit log is sent successfully + */ +async function sendAppAssetsDeployedAuditLog ({ accessToken, cliCommandFlags, appInfo, opItems, env }) { + const logEvent = getAuditLogEvent({ cliCommandFlags, appInfo, operation: OPERATIONS.AB_APP_ASSETS_DEPLOYED }) + logEvent.data.opItems = opItems + return publishAuditLogs({ accessToken, logEvent, env }) +} + +/** + * Send audit log event for app assets undeployment + * + * @param {AuditLogParams} params Parameters object + * @returns {Promise} Promise that resolves when the audit log is sent successfully + */ +async function sendAppAssetsUndeployedAuditLog ({ accessToken, cliCommandFlags, appInfo, env }) { + const logEvent = getAuditLogEvent({ cliCommandFlags, appInfo, operation: OPERATIONS.AB_APP_ASSETS_UNDEPLOYED }) + return publishAuditLogs({ accessToken, logEvent, env }) +} + module.exports = { OPERATIONS, AUDIT_SERVICE_ENDPOINTS, From 9351129365494bdb5b9ee367cb92fd71a3b0c7d9 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Wed, 30 Apr 2025 17:43:48 +0800 Subject: [PATCH 06/21] temp: use version of generator that doesn't fail with no-logged in use case --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0c20e444f..4cf2d2d48 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@adobe/aio-lib-runtime": "^7.1.0", "@adobe/aio-lib-templates": "^3", "@adobe/aio-lib-web": "^7", - "@adobe/generator-aio-app": "^8", + "@adobe/generator-aio-app": "github:adobe/generator-aio-app#master", "@adobe/generator-app-common-lib": "^2", "@adobe/inquirer-table-checkbox": "^2", "@oclif/core": "^2.11.6", From 3e0c6e57b274463db2f3c0a606bff58dce778af0 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Wed, 30 Apr 2025 17:49:40 +0800 Subject: [PATCH 07/21] fix jsdoc --- src/lib/audit-logger.js | 52 +++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src/lib/audit-logger.js b/src/lib/audit-logger.js index 864461d20..d4464e408 100644 --- a/src/lib/audit-logger.js +++ b/src/lib/audit-logger.js @@ -28,35 +28,43 @@ const AUDIT_SERVICE_ENDPOINTS = { prod: 'http://127.0.0.1:3000/audit-log-api/event-post' } +/** + * @typedef {object} AppInfo + * @property {string} name - Application name + * @property {string} version - Application version + * @property {object} project - Project details containing org and workspace information + * @property {string} [runtimeNamespace] - Optional runtime namespace (for non-logged in use case) + */ + /** * @typedef {object} AuditLogParams - * @property {string} accessToken - valid access token - * @property {object} cliCommandFlags - cli flags - * @property {object} project - project details - * @property {Array} [opItems] - list of deployed files (only for assets deployment) - * @property {string} [env='prod'] - valid env stage|prod + * @property {string} accessToken - Valid access token for authentication + * @property {object} cliCommandFlags - CLI command flags and options + * @property {AppInfo} appInfo - Application information including project details, name, and version + * @property {Array} [opItems] - List of deployed files (only for assets deployment) + * @property {string} [env='prod'] - Environment to use: 'stage' or 'prod' */ /** * @typedef {object} PublishAuditLogParams - * @property {string} accessToken - valid access token - * @property {object} logEvent - logEvent details - * @property {string} [env='prod'] - valid env stage|prod + * @property {string} accessToken - Valid access token for authentication + * @property {object} logEvent - Audit log event details to be published + * @property {string} [env='prod'] - Environment to use: 'stage' or 'prod' */ /** * @typedef {object} GetAuditLogEventParams - * @property {object} cliCommandFlags - CLI command flags - * @property {object} project - Project details containing org and workspace information - * @property {string} operation - One of: ab_app_deploy, ab_app_undeploy, ab_app_assets_deployed, ab_app_assets_undeployed - * @property {string} [runtimeNamespace] - Optional runtime namespace (for non-logged in use case) + * @property {object} cliCommandFlags - CLI command flags and options + * @property {AppInfo} appInfo - Application information containing project details, name, version, and optional runtime namespace + * @property {string} operation - Operation type: 'ab_app_deploy', 'ab_app_undeploy', 'ab_app_assets_deployed', or 'ab_app_assets_undeployed' */ /** * Publish audit log events to audit service * - * @param {PublishAuditLogParams} params Parameters object + * @param {PublishAuditLogParams} params - Parameters object containing access token, log event, and environment * @returns {Promise} Promise that resolves when the audit log is sent successfully + * @throws {Error} If the audit log request fails */ async function publishAuditLogs ({ accessToken, logEvent, env = 'prod' }) { const url = AUDIT_SERVICE_ENDPOINTS[env] ?? AUDIT_SERVICE_ENDPOINTS.prod @@ -81,9 +89,9 @@ async function publishAuditLogs ({ accessToken, logEvent, env = 'prod' }) { /** * Creates an audit log event object * - * @param {GetAuditLogEventParams} params Parameters object - * @returns {object} logEvent object containing audit log details - * @throws {Error} if project, project.org, or project.workspace is missing, or if operation is invalid + * @param {GetAuditLogEventParams} params - Parameters object containing CLI flags, operation type, and app info + * @returns {object} Log event object containing audit log details + * @throws {Error} If project or runtimeNamespace is missing, or if operation is invalid */ function getAuditLogEvent ({ cliCommandFlags, operation, appInfo }) { const { project, runtimeNamespace } = appInfo @@ -150,8 +158,9 @@ function getAuditLogEvent ({ cliCommandFlags, operation, appInfo }) { /** * Send audit log event for app deployment * - * @param {AuditLogParams} params Parameters object + * @param {AuditLogParams} params - Parameters object containing access token, CLI flags, app info, and environment * @returns {Promise} Promise that resolves when the audit log is sent successfully + * @throws {Error} If the audit log request fails */ async function sendAppDeployAuditLog ({ accessToken, cliCommandFlags, appInfo, env }) { const logEvent = getAuditLogEvent({ cliCommandFlags, appInfo, operation: OPERATIONS.AB_APP_DEPLOY }) @@ -161,8 +170,9 @@ async function sendAppDeployAuditLog ({ accessToken, cliCommandFlags, appInfo, e /** * Send audit log event for app undeployment * - * @param {AuditLogParams} params Parameters object + * @param {AuditLogParams} params - Parameters object containing access token, CLI flags, app info, and environment * @returns {Promise} Promise that resolves when the audit log is sent successfully + * @throws {Error} If the audit log request fails */ async function sendAppUndeployAuditLog ({ accessToken, cliCommandFlags, appInfo, env }) { const logEvent = getAuditLogEvent({ cliCommandFlags, appInfo, operation: OPERATIONS.AB_APP_UNDEPLOY }) @@ -172,8 +182,9 @@ async function sendAppUndeployAuditLog ({ accessToken, cliCommandFlags, appInfo, /** * Send audit log event for app assets deployment * - * @param {AuditLogParams} params Parameters object + * @param {AuditLogParams} params - Parameters object containing access token, CLI flags, app info, operation items, and environment * @returns {Promise} Promise that resolves when the audit log is sent successfully + * @throws {Error} If the audit log request fails */ async function sendAppAssetsDeployedAuditLog ({ accessToken, cliCommandFlags, appInfo, opItems, env }) { const logEvent = getAuditLogEvent({ cliCommandFlags, appInfo, operation: OPERATIONS.AB_APP_ASSETS_DEPLOYED }) @@ -184,8 +195,9 @@ async function sendAppAssetsDeployedAuditLog ({ accessToken, cliCommandFlags, ap /** * Send audit log event for app assets undeployment * - * @param {AuditLogParams} params Parameters object + * @param {AuditLogParams} params - Parameters object containing access token, CLI flags, app info, and environment * @returns {Promise} Promise that resolves when the audit log is sent successfully + * @throws {Error} If the audit log request fails */ async function sendAppAssetsUndeployedAuditLog ({ accessToken, cliCommandFlags, appInfo, env }) { const logEvent = getAuditLogEvent({ cliCommandFlags, appInfo, operation: OPERATIONS.AB_APP_ASSETS_UNDEPLOYED }) From 2a1b64b411b11584d2e36093801ccbcec5254e8f Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Wed, 30 Apr 2025 18:43:56 +0800 Subject: [PATCH 08/21] update tests --- src/lib/audit-logger.js | 2 +- test/commands/app/deploy.test.js | 384 +++++++++++++++---------- test/commands/app/undeploy.test.js | 87 ++++-- test/commands/lib/audit-logger.test.js | 108 +++++-- test/data-mocks/config-loader.js | 38 +++ 5 files changed, 426 insertions(+), 193 deletions(-) diff --git a/src/lib/audit-logger.js b/src/lib/audit-logger.js index d4464e408..e324cf279 100644 --- a/src/lib/audit-logger.js +++ b/src/lib/audit-logger.js @@ -115,7 +115,7 @@ function getAuditLogEvent ({ cliCommandFlags, operation, appInfo }) { workspaceName = project.workspace.name } else { // non-logged in use case const parsedNamespace = parseNamespaceString(runtimeNamespace) - workspaceName = parsedNamespace.workspace ?? 'Production' + workspaceName = parsedNamespace?.workspace ?? 'Production' } let logStrMsg diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index 3e19f4f4e..19575c13f 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -156,6 +156,8 @@ afterAll(() => { jest.resetAllMocks() }) +let command + beforeEach(() => { helpers.writeConfig.mockReset() helpers.runInProcess.mockReset() @@ -187,6 +189,37 @@ beforeEach(() => { } }) LogForwarding.init.mockResolvedValue(mockLogForwarding) + + command = new TheCommand([]) + command.error = jest.fn() + command.log = jest.fn() + command.appConfig = cloneDeep(mockConfigData) + command.appConfig.actions = { dist: 'actions' } + command.appConfig.web.distProd = 'dist' + command.config = { runCommand: jest.fn(), runHook: jest.fn() } + command.buildOneExt = jest.fn() + command.getFullConfig = jest.fn().mockResolvedValue({ + aio: { + project: { + workspace: { + name: 'test-workspace' + } + } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' + } + }) + command.getAppExtConfigs = jest.fn().mockResolvedValue(createAppConfig(command.appConfig)) + command.getLibConsoleCLI = jest.fn(() => mockLibConsoleCLI) + + mockRuntimeLib.deployActions.mockResolvedValue({ actions: [] }) + mockWebLib.bundle.mockResolvedValue({ run: mockBundleFunc }) + + mockLogForwarding.isLocalConfigChanged.mockReturnValue(true) + const config = new LogForwarding.LogForwardingConfig('destination', { field: 'value' }) + mockLogForwarding.getLocalConfigWithSecrets.mockReturnValue(config) }) test('exports', async () => { @@ -259,61 +292,26 @@ test('flags', async () => { }) describe('run', () => { - let command - - beforeEach(() => { - command = new TheCommand([]) - command.error = jest.fn() - command.log = jest.fn() - command.appConfig = cloneDeep(mockConfigData) - command.appConfig.actions = { dist: 'actions' } - command.appConfig.web.distProd = 'dist' - command.config = { runCommand: jest.fn(), runHook: jest.fn() } - command.buildOneExt = jest.fn() - command.getAppExtConfigs = jest.fn() - command.getLibConsoleCLI = jest.fn(() => mockLibConsoleCLI) - command.getFullConfig = jest.fn().mockReturnValue({ - aio: { - project: { - workspace: { - name: 'foo' - } - } - } - }) - - mockRuntimeLib.deployActions.mockResolvedValue({ actions: [] }) - mockWebLib.bundle.mockResolvedValue({ run: mockBundleFunc }) - - mockLogForwarding.isLocalConfigChanged.mockReturnValue(true) - const config = new LogForwarding.LogForwardingConfig('destination', { field: 'value' }) - mockLogForwarding.getLocalConfigWithSecrets.mockReturnValue(config) - }) - - afterEach(() => { - jest.clearAllMocks() - }) - test('build & deploy an App with no flags', async () => { - command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) + command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig, 'app-exc-nui')) await command.run() expect(command.error).toHaveBeenCalledTimes(0) - expect(command.buildOneExt).toHaveBeenCalledTimes(1) + expect(command.buildOneExt).toHaveBeenCalledTimes(3) expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) - expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) + expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(2) }) test('build & deploy an App verbose', async () => { - const appConfig = createAppConfig(command.appConfig) + const appConfig = createAppConfig(command.appConfig, 'app-exc-nui') command.getAppExtConfigs.mockResolvedValueOnce(appConfig) command.argv = ['-v'] await command.run() expect(command.error).toHaveBeenCalledTimes(0) expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) - expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) - expect(command.buildOneExt).toHaveBeenCalledTimes(1) + expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(2) + expect(command.buildOneExt).toHaveBeenCalledTimes(3) expect(command.buildOneExt).toHaveBeenCalledWith('application', appConfig.application, expect.objectContaining({ verbose: true, 'force-build': true }), @@ -350,6 +348,10 @@ describe('run', () => { id: '1111' } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) @@ -376,6 +378,10 @@ describe('run', () => { id: '1111' } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) @@ -576,7 +582,6 @@ describe('run', () => { [['--no-log-forwarding-update', '--no-actions']] ])('no log forwarding update due to %s arg(s) specified', async (args) => { const appConfig = createAppConfig(command.appConfig) - command.getAppExtConfigs.mockResolvedValueOnce(appConfig) command.argv = args await command.run() expect(command.error).toHaveBeenCalledTimes(0) @@ -739,6 +744,10 @@ describe('run', () => { id: '1111' } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) const noScriptFound = undefined @@ -768,6 +777,10 @@ describe('run', () => { id: '1111' } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) mockExtRegExcShellPayload() @@ -907,6 +920,15 @@ describe('run', () => { mockGetProject() command.getFullConfig.mockResolvedValue({ aio: { + project: { + workspace: { + name: 'foo' + } + } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) mockExtRegExcShellPayload() @@ -935,6 +957,10 @@ describe('run', () => { id: '1111' } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) mockExtRegExcShellPayload() @@ -962,6 +988,10 @@ describe('run', () => { id: '1111' } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) mockExtRegExcShellPayload() @@ -991,6 +1021,10 @@ describe('run', () => { id: '1111' } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) mockExtRegExcShellPayloadFailure() @@ -1018,6 +1052,10 @@ describe('run', () => { id: '1111' } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) command.argv = [] @@ -1045,6 +1083,10 @@ describe('run', () => { id: '1111' } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) mockExtRegExcShellPayload() @@ -1072,95 +1114,13 @@ describe('run', () => { id: '1111' } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) mockExtRegExcShellAndNuiPayload() - command.argv = [] - await command.run() - expect(command.error).toHaveBeenCalledTimes(0) - expect(helpers.buildExcShellViewExtensionMetadata).toHaveBeenCalledTimes(1) - expect(mockLibConsoleCLI.updateExtensionPoints).toHaveBeenCalledTimes(0) - expect(mockLibConsoleCLI.updateExtensionPointsWithoutOverwrites).toHaveBeenCalledTimes(1) - }) - - test('publish phase (no force, nui payload + no view operation)', async () => { - mockGetProject() - mockGetExtensionPointsRetractedApp() - command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig, 'app-exc-nui')) - command.getFullConfig.mockResolvedValue({ - aio: { - project: { - workspace: { - name: 'foo' - }, - org: { - id: '1111' - } - } - } - }) - const payload = { - endpoints: { - 'dx/excshell/1': { - another: [ - ] - } - } - } - helpers.buildExtensionPointPayloadWoMetadata.mockReturnValueOnce(payload) - mockLibConsoleCLI.updateExtensionPointsWithoutOverwrites.mockReturnValueOnce(payload) - mockLibConsoleCLI.updateExtensionPoints.mockReturnValueOnce(payload) - command.argv = [] - await command.run() - expect(command.error).toHaveBeenCalledTimes(0) - expect(helpers.buildExcShellViewExtensionMetadata).toHaveBeenCalledTimes(0) - expect(mockLibConsoleCLI.updateExtensionPoints).toHaveBeenCalledTimes(0) - expect(mockLibConsoleCLI.updateExtensionPointsWithoutOverwrites).toHaveBeenCalledTimes(1) - }) - - test('publish phase (--force-publish, exc+nui payload)', async () => { - mockGetProject() - mockGetExtensionPointsRetractedApp() - command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig, 'exc')) - command.getFullConfig.mockResolvedValue({ - aio: { - project: { - workspace: { - name: 'foo' - }, - org: { - id: '1111' - } - } - } - }) - - mockExtRegExcShellAndNuiPayload() - command.argv = ['--force-publish'] - await command.run() - expect(command.error).toHaveBeenCalledTimes(0) - expect(helpers.buildExcShellViewExtensionMetadata).toHaveBeenCalledTimes(1) - expect(mockLibConsoleCLI.updateExtensionPoints).toHaveBeenCalledTimes(1) - expect(mockLibConsoleCLI.updateExtensionPointsWithoutOverwrites).toHaveBeenCalledTimes(0) - }) - - test('app hook sequence', async () => { - const appConfig = createAppConfig(command.appConfig) - command.getAppExtConfigs.mockResolvedValueOnce(appConfig) - - // set hooks (command the same as hook name, for easy reference) - appConfig.application.hooks = { - 'pre-app-deploy': 'pre-app-deploy', - 'deploy-actions': 'deploy-actions', - 'deploy-static': 'deploy-static', - 'post-app-deploy': 'post-app-deploy' - } - - const scriptSequence = [] - helpers.runInProcess.mockImplementation(script => { - scriptSequence.push(script) - }) - command.argv = [] await command.run() expect(command.error).toHaveBeenCalledTimes(0) @@ -1306,6 +1266,10 @@ describe('run', () => { name: mockWorkspaceName } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } } command.getFullConfig = jest.fn().mockReturnValue(fullConfig) @@ -1317,15 +1281,45 @@ describe('run', () => { expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(1) expect(authHelper.setRuntimeApiHostAndAuthHandler).toHaveBeenCalledTimes(1) - expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledWith( - { - accessToken: mockToken, - cliCommandFlags: expect.any(Object), - opItems: expect.any(Array), - project: fullConfig.aio.project, - env: mockEnv - } - ) + expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledWith({ + accessToken: mockToken, + appInfo: { + name: 'test-app', + version: '1.0.0', + runtimeNamespace: undefined, + project: { + id: mockProject, + org: { + id: mockOrg + }, + workspace: { + id: mockWorkspaceId, + name: mockWorkspaceName + } + } + }, + cliCommandFlags: { + actions: true, + build: true, + 'content-hash': true, + 'force-build': true, + 'force-deploy': false, + 'force-events': false, + 'force-publish': false, + 'log-forwarding-update': true, + open: false, + publish: false, + 'web-assets': true, + 'web-optimize': false + }, + env: mockEnv, + opItems: [ + '3 Javascript file(s)', + '2 CSS file(s)', + '5 image(s)', + '1 HTML page(s)' + ] + }) process.env.IS_DEPLOY_SERVICE_ENABLED = false }) @@ -1353,6 +1347,10 @@ describe('run', () => { name: mockWorkspaceName } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } } command.getFullConfig = jest.fn().mockReturnValue(fullConfig) @@ -1363,15 +1361,45 @@ describe('run', () => { expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(1) - expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledWith( - { - accessToken: mockToken, - cliCommandFlags: expect.any(Object), - opItems: expect.any(Array), - project: fullConfig.aio.project, - env: mockEnv - } - ) + expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledWith({ + accessToken: mockToken, + appInfo: { + name: 'test-app', + version: '1.0.0', + runtimeNamespace: undefined, + project: { + id: mockProject, + org: { + id: mockOrg + }, + workspace: { + id: mockWorkspaceId, + name: mockWorkspaceName + } + } + }, + cliCommandFlags: { + actions: true, + build: true, + 'content-hash': true, + 'force-build': true, + 'force-deploy': false, + 'force-events': false, + 'force-publish': false, + 'log-forwarding-update': true, + open: false, + publish: false, + 'web-assets': true, + 'web-optimize': false + }, + env: mockEnv, + opItems: [ + '3 Javascript file(s)', + '2 CSS file(s)', + '5 image(s)', + '1 HTML page(s)' + ] + }) }) test('Do not send audit logs for successful app deploy, if case of no token', async () => { @@ -1399,6 +1427,10 @@ describe('run', () => { name: mockWorkspaceName } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } } command.getFullConfig = jest.fn().mockReturnValue(fullConfig) @@ -1438,6 +1470,10 @@ describe('run', () => { name: mockWorkspaceName } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } } command.getFullConfig = jest.fn().mockReturnValue(fullConfig) @@ -1448,15 +1484,45 @@ describe('run', () => { expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) expect(helpers.getFilesCountWithExtension).toHaveBeenCalledTimes(2) - expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledWith( - { - accessToken: mockToken, - cliCommandFlags: expect.any(Object), - opItems: expect.any(Array), - project: fullConfig.aio.project, - env: mockEnv - } - ) + expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledWith({ + accessToken: mockToken, + appInfo: { + name: 'test-app', + version: '1.0.0', + runtimeNamespace: undefined, + project: { + id: mockProject, + org: { + id: mockOrg + }, + workspace: { + id: mockWorkspaceId, + name: mockWorkspaceName + } + } + }, + cliCommandFlags: { + actions: true, + build: true, + 'content-hash': true, + 'force-build': true, + 'force-deploy': false, + 'force-events': false, + 'force-publish': false, + 'log-forwarding-update': true, + open: false, + publish: false, + 'web-assets': true, + 'web-optimize': false + }, + env: mockEnv, + opItems: [ + '3 Javascript file(s)', + '2 CSS file(s)', + '5 image(s)', + '1 HTML page(s)' + ] + }) }) test('Should deploy successfully even if Audit log service is unavailable', async () => { @@ -1482,6 +1548,10 @@ describe('run', () => { name: mockWorkspaceName } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) @@ -1529,6 +1599,10 @@ describe('run', () => { name: mockWorkspaceName } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) diff --git a/test/commands/app/undeploy.test.js b/test/commands/app/undeploy.test.js index 77e4bb5f0..41593b915 100644 --- a/test/commands/app/undeploy.test.js +++ b/test/commands/app/undeploy.test.js @@ -13,6 +13,7 @@ governing permissions and limitations under the License. const TheCommand = require('../../../src/commands/app/undeploy') const BaseCommand = require('../../../src/BaseCommand') const dataMocks = require('../../data-mocks/config-loader') +const { cloneDeep } = require('lodash') jest.mock('../../../src/lib/app-helper.js') const helpers = require('../../../src/lib/app-helper.js') @@ -30,6 +31,9 @@ const mockConfigData = { app: { hasFrontend: true, hasBackend: true + }, + web: { + distProd: 'dist' } } @@ -146,22 +150,29 @@ describe('run', () => { command = new TheCommand([]) command.error = jest.fn() command.log = jest.fn() - command.appConfig = mockConfigData - command.config = { - runCommand: jest.fn(), - runHook: jest.fn() - } - command.getLibConsoleCLI = jest.fn(() => mockLibConsoleCLI) - command.getAppExtConfigs = jest.fn() - command.getFullConfig = jest.fn().mockReturnValue({ + command.appConfig = cloneDeep(mockConfigData) + command.appConfig.actions = { dist: 'actions' } + command.appConfig.web.distProd = 'dist' + command.config = { runCommand: jest.fn(), runHook: jest.fn() } + command.buildOneExt = jest.fn() + command.getFullConfig = jest.fn().mockResolvedValue({ aio: { project: { workspace: { - name: 'foo' + name: 'Production' + }, + org: { + id: '1111' } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) + command.getLibConsoleCLI = jest.fn(() => mockLibConsoleCLI) + command.getAppExtConfigs = jest.fn() }) afterEach(() => { @@ -348,6 +359,10 @@ describe('run', () => { name: 'foo' } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) const payload = { @@ -379,6 +394,10 @@ describe('run', () => { name: 'foo' } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) @@ -496,6 +515,10 @@ describe('run', () => { name: mockWorkspaceName } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) @@ -528,6 +551,10 @@ describe('run', () => { name: mockWorkspaceName } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } } command.getFullConfig = jest.fn().mockReturnValue(fullConfig) @@ -538,14 +565,32 @@ describe('run', () => { expect(mockRuntimeLib.undeployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(1) expect(auditLogger.sendAppAssetsUndeployedAuditLog.mock.calls.length).toBe(1) - expect(auditLogger.sendAppAssetsUndeployedAuditLog).toHaveBeenCalledWith( - { - accessToken: mockToken, - cliCommandFlags: expect.any(Object), - project: fullConfig.aio.project, - env: mockEnv - } - ) + expect(auditLogger.sendAppAssetsUndeployedAuditLog).toHaveBeenCalledWith({ + accessToken: mockToken, + appInfo: { + name: 'test-app', + version: '1.0.0', + runtimeNamespace: undefined, + project: { + id: mockProject, + org: { + id: mockOrg + }, + workspace: { + id: mockWorkspaceId, + name: mockWorkspaceName + } + } + }, + cliCommandFlags: { + actions: true, + events: true, + 'force-unpublish': false, + unpublish: false, + 'web-assets': true + }, + env: mockEnv + }) }) test('Do not Send audit logs for successful app undeploy if case of no-token', async () => { @@ -566,6 +611,10 @@ describe('run', () => { name: mockWorkspaceName } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) @@ -596,6 +645,10 @@ describe('run', () => { name: mockWorkspaceName } } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' } }) command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) diff --git a/test/commands/lib/audit-logger.test.js b/test/commands/lib/audit-logger.test.js index 62fa191fa..6de9ee858 100644 --- a/test/commands/lib/audit-logger.test.js +++ b/test/commands/lib/audit-logger.test.js @@ -32,6 +32,11 @@ describe('audit-logger', () => { name: 'fake-workspace' } } + const mockAppInfo = { + name: 'test-app', + version: '1.0.0', + project: mockProject + } const mockCliFlags = { flag1: 'value1' } beforeEach(() => { @@ -39,8 +44,12 @@ describe('audit-logger', () => { }) describe('getAuditLogEvent', () => { - it('should create a valid audit log event for app deploy', () => { - const event = getAuditLogEvent(mockCliFlags, mockProject, OPERATIONS.AB_APP_DEPLOY) + it('should create a valid audit log event for app deploy (logged in)', () => { + const event = getAuditLogEvent({ + cliCommandFlags: mockCliFlags, + operation: OPERATIONS.AB_APP_DEPLOY, + appInfo: mockAppInfo + }) expect(event).toEqual({ orgId: 'fake-org-id', @@ -48,6 +57,36 @@ describe('audit-logger', () => { workspaceId: 'fake-workspace-id', workspaceName: 'fake-workspace', operation: OPERATIONS.AB_APP_DEPLOY, + appName: 'test-app', + appVersion: '1.0.0', + objectName: 'test-app', + timestamp: expect.any(Number), + data: { + cliCommandFlags: mockCliFlags, + opDetailsStr: expect.stringContaining('Starting deployment for the App Builder application') + } + }) + }) + + it('should create a valid audit log event for app deploy (non-logged in)', () => { + const nonLoggedInAppInfo = { + name: 'test-app', + version: '1.0.0', + runtimeNamespace: 'test-namespace' + } + const event = getAuditLogEvent({ + cliCommandFlags: mockCliFlags, + operation: OPERATIONS.AB_APP_DEPLOY, + appInfo: nonLoggedInAppInfo + }) + + expect(event).toEqual({ + runtimeNamespace: 'test-namespace', + workspaceName: 'Production', + operation: OPERATIONS.AB_APP_DEPLOY, + appName: 'test-app', + appVersion: '1.0.0', + objectName: 'test-app', timestamp: expect.any(Number), data: { cliCommandFlags: mockCliFlags, @@ -57,7 +96,11 @@ describe('audit-logger', () => { }) it('should create a valid audit log event for app undeploy', () => { - const event = getAuditLogEvent(mockCliFlags, mockProject, OPERATIONS.AB_APP_UNDEPLOY) + const event = getAuditLogEvent({ + cliCommandFlags: mockCliFlags, + operation: OPERATIONS.AB_APP_UNDEPLOY, + appInfo: mockAppInfo + }) expect(event).toEqual({ orgId: 'fake-org-id', @@ -65,6 +108,9 @@ describe('audit-logger', () => { workspaceId: 'fake-workspace-id', workspaceName: 'fake-workspace', operation: OPERATIONS.AB_APP_UNDEPLOY, + appName: 'test-app', + appVersion: '1.0.0', + objectName: 'test-app', timestamp: expect.any(Number), data: { cliCommandFlags: mockCliFlags, @@ -73,26 +119,48 @@ describe('audit-logger', () => { }) }) - it('should throw error if project is missing', () => { - expect(() => getAuditLogEvent(mockCliFlags, null, OPERATIONS.AB_APP_DEPLOY)) - .toThrow('Project is required') + it('should throw error if neither project nor runtimeNamespace is provided', () => { + const invalidAppInfo = { + name: 'test-app', + version: '1.0.0' + } + expect(() => getAuditLogEvent({ + cliCommandFlags: mockCliFlags, + operation: OPERATIONS.AB_APP_DEPLOY, + appInfo: invalidAppInfo + })).toThrow('Either project or runtimeNamespace is required') }) it('should throw error if project org is missing', () => { - const invalidProject = { ...mockProject, org: null } - expect(() => getAuditLogEvent(mockCliFlags, invalidProject, OPERATIONS.AB_APP_DEPLOY)) - .toThrow('Project org is required') + const invalidAppInfo = { + ...mockAppInfo, + project: { ...mockProject, org: null } + } + expect(() => getAuditLogEvent({ + cliCommandFlags: mockCliFlags, + operation: OPERATIONS.AB_APP_DEPLOY, + appInfo: invalidAppInfo + })).toThrow('Project org is required') }) it('should throw error if project workspace is missing', () => { - const invalidProject = { ...mockProject, workspace: null } - expect(() => getAuditLogEvent(mockCliFlags, invalidProject, OPERATIONS.AB_APP_DEPLOY)) - .toThrow('Project workspace is required') + const invalidAppInfo = { + ...mockAppInfo, + project: { ...mockProject, workspace: null } + } + expect(() => getAuditLogEvent({ + cliCommandFlags: mockCliFlags, + operation: OPERATIONS.AB_APP_DEPLOY, + appInfo: invalidAppInfo + })).toThrow('Project workspace is required') }) it('should throw error for invalid operation', () => { - expect(() => getAuditLogEvent(mockCliFlags, mockProject, 'invalid_operation')) - .toThrow('Invalid operation: invalid_operation') + expect(() => getAuditLogEvent({ + cliCommandFlags: mockCliFlags, + operation: 'invalid_operation', + appInfo: mockAppInfo + })).toThrow('Invalid operation: invalid_operation') }) }) @@ -103,7 +171,7 @@ describe('audit-logger', () => { await sendAppDeployAuditLog({ accessToken: mockAccessToken, cliCommandFlags: mockCliFlags, - project: mockProject + appInfo: mockAppInfo }) expect(fetch).toHaveBeenCalledWith( @@ -126,7 +194,7 @@ describe('audit-logger', () => { await sendAppUndeployAuditLog({ accessToken: mockAccessToken, cliCommandFlags: mockCliFlags, - project: mockProject + appInfo: mockAppInfo }) expect(fetch).toHaveBeenCalledWith( @@ -150,7 +218,7 @@ describe('audit-logger', () => { await sendAppAssetsDeployedAuditLog({ accessToken: mockAccessToken, cliCommandFlags: mockCliFlags, - project: mockProject, + appInfo: mockAppInfo, opItems: mockOpItems }) @@ -175,7 +243,7 @@ describe('audit-logger', () => { await sendAppAssetsUndeployedAuditLog({ accessToken: mockAccessToken, cliCommandFlags: mockCliFlags, - project: mockProject + appInfo: mockAppInfo }) expect(fetch).toHaveBeenCalledWith( @@ -201,7 +269,7 @@ describe('audit-logger', () => { await expect(sendAppDeployAuditLog({ accessToken: mockAccessToken, cliCommandFlags: mockCliFlags, - project: mockProject + appInfo: mockAppInfo })).rejects.toThrow('Failed to send audit log - 500 Internal Server Error') }) @@ -211,7 +279,7 @@ describe('audit-logger', () => { await sendAppDeployAuditLog({ accessToken: mockAccessToken, cliCommandFlags: mockCliFlags, - project: mockProject, + appInfo: mockAppInfo, env: 'invalid-env' }) diff --git a/test/data-mocks/config-loader.js b/test/data-mocks/config-loader.js index 970507d62..70274427a 100644 --- a/test/data-mocks/config-loader.js +++ b/test/data-mocks/config-loader.js @@ -480,3 +480,41 @@ module.exports = (appFixtureName, mockedAIOConfig, rewriteMockConfig = {}) => { aio: mockedAIOConfig } } + +const buildExtensionPointPayloadWoMetadata = (deployConfigs) => { + const payload = { + endpoints: {} + } + + // Add excshell extension point if present + if (deployConfigs['dx/excshell/1']) { + payload.endpoints['dx/excshell/1'] = { + view: [{ + name: 'test-app', + url: 'https://adobeio-static.net/test-app/index.html', + metadata: { + name: 'test-app', + description: 'Test App', + version: '1.0.0' + } + }] + } + } + + // Add asset compute worker extension point if present + if (deployConfigs['dx/asset-compute/worker/1']) { + payload.endpoints['dx/asset-compute/worker/1'] = { + workerProcess: [] + } + } + + return payload +} + +const buildExcShellViewExtensionMetadata = async (libConsoleCLI, aioConfig) => { + return { + name: 'test-app', + description: 'Test App', + version: '1.0.0' + } +} From 2fa99ee5e551d88ba5ee7434e0252b3e96bdfe49 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Wed, 30 Apr 2025 18:56:28 +0800 Subject: [PATCH 09/21] update tests --- test/commands/app/deploy.test.js | 62 ++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index 19575c13f..272817ad7 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -294,6 +294,15 @@ test('flags', async () => { describe('run', () => { test('build & deploy an App with no flags', async () => { command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig, 'app-exc-nui')) + helpers.buildExtensionPointPayloadWoMetadata.mockReturnValueOnce({ + endpoints: { + 'dx/excshell/1': { + view: [{ + metadata: {} + }] + } + } + }) await command.run() expect(command.error).toHaveBeenCalledTimes(0) @@ -305,6 +314,15 @@ describe('run', () => { test('build & deploy an App verbose', async () => { const appConfig = createAppConfig(command.appConfig, 'app-exc-nui') command.getAppExtConfigs.mockResolvedValueOnce(appConfig) + helpers.buildExtensionPointPayloadWoMetadata.mockReturnValueOnce({ + endpoints: { + 'dx/excshell/1': { + view: [{ + metadata: {} + }] + } + } + }) command.argv = ['-v'] await command.run() @@ -1124,7 +1142,8 @@ describe('run', () => { command.argv = [] await command.run() expect(command.error).toHaveBeenCalledTimes(0) - expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) + // For app-exc-nui config, we have 2 extensions that need web assets deployed + expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(2) expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(helpers.runInProcess).toHaveBeenCalledTimes(4) @@ -1440,7 +1459,46 @@ describe('run', () => { expect(command.error).toHaveBeenCalledTimes(0) expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) - expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(0) + expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(1) + expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledWith({ + accessToken: mockToken, + appInfo: { + name: 'test-app', + version: '1.0.0', + runtimeNamespace: undefined, + project: { + id: mockProject, + org: { + id: mockOrg + }, + workspace: { + id: mockWorkspaceId, + name: mockWorkspaceName + } + } + }, + cliCommandFlags: { + actions: true, + build: true, + 'content-hash': true, + 'force-build': true, + 'force-deploy': false, + 'force-events': false, + 'force-publish': false, + 'log-forwarding-update': true, + open: false, + publish: false, + 'web-assets': true, + 'web-optimize': false + }, + env: mockEnv, + opItems: [ + '3 Javascript file(s)', + '2 CSS file(s)', + '5 image(s)', + '1 HTML page(s)' + ] + }) }) test('Send audit logs for successful app deploy + web assets', async () => { From 875de59bd08f37a9a74e077ddbf054addc5d2ecd Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Wed, 30 Apr 2025 20:48:57 +0800 Subject: [PATCH 10/21] improve coverage --- test/commands/app/deploy.test.js | 186 +++++++++++---------- test/commands/app/undeploy.test.js | 112 +++++++++++-- test/commands/lib/app-helper.test.js | 63 +++++++ test/lib/audit-logger.test.js | 237 +++++++++++++++++++++++++++ 4 files changed, 494 insertions(+), 104 deletions(-) create mode 100644 test/lib/audit-logger.test.js diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index 272817ad7..146317131 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -304,10 +304,11 @@ describe('run', () => { } }) + mockExtRegExcShellPayload() await command.run() expect(command.error).toHaveBeenCalledTimes(0) expect(command.buildOneExt).toHaveBeenCalledTimes(3) - expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(3) // 3 extensions expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(2) }) @@ -324,10 +325,11 @@ describe('run', () => { } }) + mockExtRegExcShellPayload() command.argv = ['-v'] await command.run() expect(command.error).toHaveBeenCalledTimes(0) - expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(3) // 3 extensions expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(2) expect(command.buildOneExt).toHaveBeenCalledTimes(3) expect(command.buildOneExt).toHaveBeenCalledWith('application', @@ -807,13 +809,13 @@ describe('run', () => { .mockResolvedValueOnce(noScriptFound) // pre-app-deploy .mockResolvedValueOnce(noScriptFound) // post-app-deploy - command.argv = ['--no-actions', '--no-web-assets'] + command.argv = ['--no-actions', '--no-web-assets', '--force-publish'] await command.run() expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(0) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(0) - expect(mockLibConsoleCLI.updateExtensionPoints).toHaveBeenCalledTimes(0) - expect(mockLibConsoleCLI.updateExtensionPointsWithoutOverwrites).toHaveBeenCalledTimes(1) + expect(mockLibConsoleCLI.updateExtensionPoints).toHaveBeenCalledTimes(1) + expect(mockLibConsoleCLI.updateExtensionPointsWithoutOverwrites).toHaveBeenCalledTimes(0) }) test('deploy (--no-actions)', async () => { @@ -1144,14 +1146,7 @@ describe('run', () => { expect(command.error).toHaveBeenCalledTimes(0) // For app-exc-nui config, we have 2 extensions that need web assets deployed expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(2) - expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) - - expect(helpers.runInProcess).toHaveBeenCalledTimes(4) - expect(scriptSequence.length).toEqual(4) - expect(scriptSequence[0]).toEqual('pre-app-deploy') - expect(scriptSequence[1]).toEqual('deploy-actions') - expect(scriptSequence[2]).toEqual('deploy-static') - expect(scriptSequence[3]).toEqual('post-app-deploy') + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(3) // 3 extensions }) test('should update log forwarding on server when local config is defined', async () => { @@ -1421,85 +1416,85 @@ describe('run', () => { }) }) - test('Do not send audit logs for successful app deploy, if case of no token', async () => { - const mockToken = null - const mockEnv = 'stage' - const mockOrg = 'mockorg' - const mockProject = 'mockproject' - const mockWorkspaceId = 'mockworkspaceid' - const mockWorkspaceName = 'mockworkspacename' - - helpers.getCliInfo.mockResolvedValueOnce({ - accessToken: mockToken, - env: mockEnv - }) - - const fullConfig = { - aio: { - project: { - id: mockProject, - org: { - id: mockOrg - }, - workspace: { - id: mockWorkspaceId, - name: mockWorkspaceName - } - } - }, - packagejson: { - name: 'test-app', - version: '1.0.0' - } - } - command.getFullConfig = jest.fn().mockReturnValue(fullConfig) - command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) - - await command.run() - expect(command.error).toHaveBeenCalledTimes(0) - expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) - expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) - expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(1) - expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledWith({ - accessToken: mockToken, - appInfo: { - name: 'test-app', - version: '1.0.0', - runtimeNamespace: undefined, - project: { - id: mockProject, - org: { - id: mockOrg - }, - workspace: { - id: mockWorkspaceId, - name: mockWorkspaceName - } - } - }, - cliCommandFlags: { - actions: true, - build: true, - 'content-hash': true, - 'force-build': true, - 'force-deploy': false, - 'force-events': false, - 'force-publish': false, - 'log-forwarding-update': true, - open: false, - publish: false, - 'web-assets': true, - 'web-optimize': false - }, - env: mockEnv, - opItems: [ - '3 Javascript file(s)', - '2 CSS file(s)', - '5 image(s)', - '1 HTML page(s)' - ] - }) - }) + // test('Do not send audit logs for successful app deploy, if case of no token', async () => { + // const mockToken = null + // const mockEnv = 'stage' + // const mockOrg = 'mockorg' + // const mockProject = 'mockproject' + // const mockWorkspaceId = 'mockworkspaceid' + // const mockWorkspaceName = 'mockworkspacename' + + // helpers.getCliInfo.mockResolvedValueOnce({ + // accessToken: mockToken, + // env: mockEnv + // }) + + // const fullConfig = { + // aio: { + // project: { + // id: mockProject, + // org: { + // id: mockOrg + // }, + // workspace: { + // id: mockWorkspaceId, + // name: mockWorkspaceName + // } + // } + // }, + // packagejson: { + // name: 'test-app', + // version: '1.0.0' + // } + // } + // command.getFullConfig = jest.fn().mockReturnValue(fullConfig) + // command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) + + // await command.run() + // expect(command.error).toHaveBeenCalledTimes(0) + // expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) + // expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) + // expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(1) + // expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledWith({ + // accessToken: mockToken, + // appInfo: { + // name: 'test-app', + // version: '1.0.0', + // runtimeNamespace: undefined, + // project: { + // id: mockProject, + // org: { + // id: mockOrg + // }, + // workspace: { + // id: mockWorkspaceId, + // name: mockWorkspaceName + // } + // } + // }, + // cliCommandFlags: { + // actions: true, + // build: true, + // 'content-hash': true, + // 'force-build': true, + // 'force-deploy': false, + // 'force-events': false, + // 'force-publish': false, + // 'log-forwarding-update': true, + // open: false, + // publish: false, + // 'web-assets': true, + // 'web-optimize': false + // }, + // env: mockEnv, + // opItems: [ + // '3 Javascript file(s)', + // '2 CSS file(s)', + // '5 image(s)', + // '1 HTML page(s)' + // ] + // }) + // }) test('Send audit logs for successful app deploy + web assets', async () => { const mockToken = 'mocktoken' @@ -1594,6 +1589,7 @@ describe('run', () => { accessToken: mockToken, env: mockEnv }) + command.getFullConfig = jest.fn().mockReturnValue({ aio: { project: { @@ -1613,13 +1609,14 @@ describe('run', () => { } }) - auditLogger.sendAppAssetsDeployedAuditLog.mockRejectedValue({ + auditLogger.sendAppDeployAuditLog.mockRejectedValue({ message: 'Internal Server Error', status: 500 }) command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) + command.argv = ['--verbose'] await command.run() expect(command.log).toHaveBeenCalledWith( expect.stringContaining('skipping publish phase...') @@ -1628,7 +1625,7 @@ describe('run', () => { expect(command.log).toHaveBeenCalledWith( expect.stringContaining('Successful deployment 🏄') ) - expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(1) + expect(auditLogger.sendAppDeployAuditLog).toHaveBeenCalledTimes(1) expect(command.error).toHaveBeenCalledTimes(0) expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) @@ -1645,6 +1642,7 @@ describe('run', () => { accessToken: mockToken, env: mockEnv }) + command.getFullConfig = jest.fn().mockReturnValue({ aio: { project: { diff --git a/test/commands/app/undeploy.test.js b/test/commands/app/undeploy.test.js index 41593b915..aeb2999d9 100644 --- a/test/commands/app/undeploy.test.js +++ b/test/commands/app/undeploy.test.js @@ -256,24 +256,40 @@ describe('run', () => { expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(0) }) - test('undeploy skip web assets', async () => { + test('should handle audit log error with verbose flag', async () => { command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig()) - - command.argv = ['--no-web-assets'] + command.warn = jest.fn() + command.argv = ['-v'] + + // Mock audit logger to throw an error + auditLogger.sendAppUndeployAuditLog.mockRejectedValueOnce(new Error('Audit log error')) + await command.run() - expect(command.error).toHaveBeenCalledTimes(0) + + // Verify error was logged with verbose flag + expect(command.warn).toHaveBeenCalledWith('Error: Audit Log Service Error: Failed to send audit log event for deployment.') + expect(command.warn).toHaveBeenCalledWith('Audit log error') + + // Verify deployment still continues expect(mockRuntimeLib.undeployActions).toHaveBeenCalledTimes(1) - expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(0) + expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(1) }) - test('undeploy skip static verbose', async () => { + test('should handle audit log error without verbose flag', async () => { command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig()) - - command.argv = ['--no-web-assets', '-v'] + command.warn = jest.fn() + + // Mock audit logger to throw an error + auditLogger.sendAppUndeployAuditLog.mockRejectedValueOnce(new Error('Audit log error')) + await command.run() - expect(command.error).toHaveBeenCalledTimes(0) + + // Verify error was not logged without verbose flag + expect(command.warn).not.toHaveBeenCalled() + + // Verify deployment still continues expect(mockRuntimeLib.undeployActions).toHaveBeenCalledTimes(1) - expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(0) + expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(1) }) test('undeploy an app with no backend', async () => { @@ -664,4 +680,80 @@ describe('run', () => { expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(1) expect(auditLogger.sendAppAssetsUndeployedAuditLog).toHaveBeenCalledTimes(1) }) + + test('Should app undeploy successfully even if Audit Log Service returns 503', async () => { + const mockOrg = 'mockorg' + const mockProject = 'mockproject' + const mockWorkspaceId = 'mockworkspaceid' + const mockWorkspaceName = 'mockworkspacename' + + command.getFullConfig = jest.fn().mockReturnValue({ + aio: { + project: { + id: mockProject, + org: { + id: mockOrg + }, + workspace: { + id: mockWorkspaceId, + name: mockWorkspaceName + } + } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' + } + }) + command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) + + auditLogger.sendAppAssetsUndeployedAuditLog.mockRejectedValue({ + message: 'Service Unavailable', + status: 503 + }) + + await command.run() + expect(command.error).toHaveBeenCalledTimes(0) + expect(mockRuntimeLib.undeployActions).toHaveBeenCalledTimes(1) + expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(1) + expect(auditLogger.sendAppAssetsUndeployedAuditLog).toHaveBeenCalledTimes(1) + }) + + test('Should app undeploy successfully even if Audit Log Service returns 504', async () => { + const mockOrg = 'mockorg' + const mockProject = 'mockproject' + const mockWorkspaceId = 'mockworkspaceid' + const mockWorkspaceName = 'mockworkspacename' + + command.getFullConfig = jest.fn().mockReturnValue({ + aio: { + project: { + id: mockProject, + org: { + id: mockOrg + }, + workspace: { + id: mockWorkspaceId, + name: mockWorkspaceName + } + } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' + } + }) + command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) + + auditLogger.sendAppAssetsUndeployedAuditLog.mockRejectedValue({ + message: 'Gateway Timeout', + status: 504 + }) + + await command.run() + expect(command.error).toHaveBeenCalledTimes(0) + expect(mockRuntimeLib.undeployActions).toHaveBeenCalledTimes(1) + expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(1) + expect(auditLogger.sendAppAssetsUndeployedAuditLog).toHaveBeenCalledTimes(1) + }) }) diff --git a/test/commands/lib/app-helper.test.js b/test/commands/lib/app-helper.test.js index f7fd53ec6..7a3487cc0 100644 --- a/test/commands/lib/app-helper.test.js +++ b/test/commands/lib/app-helper.test.js @@ -873,3 +873,66 @@ describe('getFilesCountWithExtension', () => { ]) }) }) + +describe('parseNamespaceString', () => { + test('should parse development namespace correctly', () => { + const namespace = 'development-12345-myapp-dev' + const result = appHelper.parseNamespaceString(namespace) + expect(result).toEqual({ + development: true, + orgId: '12345', + appName: 'myapp', + workspace: 'dev' + }) + }) + + test('should parse production namespace correctly', () => { + const namespace = '12345-myapp-prod' + const result = appHelper.parseNamespaceString(namespace) + expect(result).toEqual({ + development: false, + orgId: '12345', + appName: 'myapp', + workspace: 'prod' + }) + }) + + test('should parse namespace without workspace correctly', () => { + const namespace = '12345-myapp' + const result = appHelper.parseNamespaceString(namespace) + expect(result).toEqual({ + development: false, + orgId: '12345', + appName: 'myapp', + workspace: undefined + }) + }) + + test('should handle case insensitive namespace', () => { + const namespace = 'DEVELOPMENT-12345-MYAPP-DEV' + const result = appHelper.parseNamespaceString(namespace) + expect(result).toEqual({ + development: true, + orgId: '12345', + appName: 'myapp', + workspace: 'dev' + }) + }) + + test('should return null for invalid namespace format', () => { + const invalidNamespaces = [ + 'invalid', + '12345', + 'myapp', + 'development', + 'development-', + '12345-', + '12345-myapp-' + ] + + invalidNamespaces.forEach(namespace => { + const result = appHelper.parseNamespaceString(namespace) + expect(result).toBeNull() + }) + }) +}) diff --git a/test/lib/audit-logger.test.js b/test/lib/audit-logger.test.js new file mode 100644 index 000000000..2ca472967 --- /dev/null +++ b/test/lib/audit-logger.test.js @@ -0,0 +1,237 @@ +const fetch = require('node-fetch') +const { sendAppDeployAuditLog, sendAppUndeployAuditLog, sendAppAssetsDeployedAuditLog, sendAppAssetsUndeployedAuditLog } = require('../../src/lib/audit-logger') + +jest.mock('node-fetch') + +describe('audit-logger', () => { + const mockAccessToken = 'mock-token' + const mockEnv = 'stage' + const mockAppInfo = { + name: 'test-app', + version: '1.0.0', + project: { + id: 'project-1', + org: { + id: 'org-1' + }, + workspace: { + id: 'workspace-1', + name: 'test-workspace' + } + } + } + const mockCliCommandFlags = { + actions: true, + build: true, + 'web-assets': true + } + const mockOpItems = [ + '3 Javascript file(s)', + '2 CSS file(s)', + '5 image(s)', + '1 HTML page(s)' + ] + + beforeEach(() => { + jest.clearAllMocks() + fetch.mockResolvedValue({ + status: 200, + text: () => Promise.resolve('success') + }) + }) + + describe('sendAppDeployAuditLog', () => { + test('should send deploy audit log successfully', async () => { + await sendAppDeployAuditLog({ + accessToken: mockAccessToken, + cliCommandFlags: mockCliCommandFlags, + appInfo: mockAppInfo, + env: mockEnv + }) + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/audit-log-api/event-post'), + expect.objectContaining({ + method: 'POST', + headers: { + Authorization: `Bearer ${mockAccessToken}`, + 'Content-type': 'application/json' + }, + body: expect.stringContaining('ab_app_deploy') + }) + ) + }) + + test('should handle missing access token', async () => { + await sendAppDeployAuditLog({ + accessToken: null, + cliCommandFlags: mockCliCommandFlags, + appInfo: mockAppInfo, + env: mockEnv + }) + + expect(fetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: '' + }) + }) + ) + }) + + test('should throw error on failed request', async () => { + fetch.mockResolvedValue({ + status: 500, + text: () => Promise.resolve('Internal Server Error') + }) + + await expect(sendAppDeployAuditLog({ + accessToken: mockAccessToken, + cliCommandFlags: mockCliCommandFlags, + appInfo: mockAppInfo, + env: mockEnv + })).rejects.toThrow('Failed to send audit log') + }) + }) + + describe('sendAppUndeployAuditLog', () => { + test('should send undeploy audit log successfully', async () => { + await sendAppUndeployAuditLog({ + accessToken: mockAccessToken, + cliCommandFlags: mockCliCommandFlags, + appInfo: mockAppInfo, + env: mockEnv + }) + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/audit-log-api/event-post'), + expect.objectContaining({ + method: 'POST', + headers: { + Authorization: `Bearer ${mockAccessToken}`, + 'Content-type': 'application/json' + }, + body: expect.stringContaining('ab_app_undeploy') + }) + ) + }) + }) + + describe('sendAppAssetsDeployedAuditLog', () => { + test('should send assets deployed audit log successfully', async () => { + await sendAppAssetsDeployedAuditLog({ + accessToken: mockAccessToken, + cliCommandFlags: mockCliCommandFlags, + appInfo: mockAppInfo, + opItems: mockOpItems, + env: mockEnv + }) + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/audit-log-api/event-post'), + expect.objectContaining({ + method: 'POST', + headers: { + Authorization: `Bearer ${mockAccessToken}`, + 'Content-type': 'application/json' + }, + body: expect.stringContaining('ab_app_assets_deployed') + }) + ) + }) + + test('should include operation items in the log event', async () => { + await sendAppAssetsDeployedAuditLog({ + accessToken: mockAccessToken, + cliCommandFlags: mockCliCommandFlags, + appInfo: mockAppInfo, + opItems: mockOpItems, + env: mockEnv + }) + + const requestBody = JSON.parse(fetch.mock.calls[0][1].body) + expect(requestBody.event.data.opItems).toEqual(mockOpItems) + }) + }) + + describe('sendAppAssetsUndeployedAuditLog', () => { + test('should send assets undeployed audit log successfully', async () => { + await sendAppAssetsUndeployedAuditLog({ + accessToken: mockAccessToken, + cliCommandFlags: mockCliCommandFlags, + appInfo: mockAppInfo, + env: mockEnv + }) + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/audit-log-api/event-post'), + expect.objectContaining({ + method: 'POST', + headers: { + Authorization: `Bearer ${mockAccessToken}`, + 'Content-type': 'application/json' + }, + body: expect.stringContaining('ab_app_assets_undeployed') + }) + ) + }) + }) + + describe('error cases', () => { + test('should throw error when project and runtimeNamespace are missing', async () => { + const invalidAppInfo = { + name: 'test-app', + version: '1.0.0' + } + + await expect(sendAppDeployAuditLog({ + accessToken: mockAccessToken, + cliCommandFlags: mockCliCommandFlags, + appInfo: invalidAppInfo, + env: mockEnv + })).rejects.toThrow('Either project or runtimeNamespace is required') + }) + + test('should throw error when project org is missing', async () => { + const invalidAppInfo = { + name: 'test-app', + version: '1.0.0', + project: { + id: 'project-1', + workspace: { + id: 'workspace-1', + name: 'test-workspace' + } + } + } + + await expect(sendAppDeployAuditLog({ + accessToken: mockAccessToken, + cliCommandFlags: mockCliCommandFlags, + appInfo: invalidAppInfo, + env: mockEnv + })).rejects.toThrow('Project org is required') + }) + + test('should throw error when project workspace is missing', async () => { + const invalidAppInfo = { + name: 'test-app', + version: '1.0.0', + project: { + id: 'project-1', + org: { + id: 'org-1' + } + } + } + + await expect(sendAppDeployAuditLog({ + accessToken: mockAccessToken, + cliCommandFlags: mockCliCommandFlags, + appInfo: invalidAppInfo, + env: mockEnv + })).rejects.toThrow('Project workspace is required') + }) + }) +}) \ No newline at end of file From 95fbf39a56d03c8dc61cc5a8d5e438fff275f713 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Wed, 30 Apr 2025 21:01:18 +0800 Subject: [PATCH 11/21] update coverage --- test/commands/app/deploy.test.js | 54 +++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index 146317131..2831cff70 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -1631,7 +1631,59 @@ describe('run', () => { expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) }) - test('Should deploy successfully even if Audit log service is unavailable (--verbose)', async () => { + test('Should deploy successfully even if Audit log service is unavailable', async () => { + const mockToken = 'mocktoken' + const mockEnv = 'stage' + const mockOrg = 'mockorg' + const mockProject = 'mockproject' + const mockWorkspaceId = 'mockworkspaceid' + const mockWorkspaceName = 'mockworkspacename' + helpers.getCliInfo.mockResolvedValueOnce({ + accessToken: mockToken, + env: mockEnv + }) + + command.getFullConfig = jest.fn().mockReturnValue({ + aio: { + project: { + id: mockProject, + org: { + id: mockOrg + }, + workspace: { + id: mockWorkspaceId, + name: mockWorkspaceName + } + } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' + } + }) + + auditLogger.sendAppAssetsDeployedAuditLog.mockRejectedValue({ + message: 'Internal Server Error', + status: 500 + }) + + command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) + + await command.run() + expect(command.log).toHaveBeenCalledWith( + expect.stringContaining('skipping publish phase...') + ) + + expect(command.log).toHaveBeenCalledWith( + expect.stringContaining('Successful deployment 🏄') + ) + expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(1) + expect(command.error).toHaveBeenCalledTimes(0) + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) + expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) + }) + + test('Should deploy successfully even if Audit log service is unavailable (--verbose', async () => { const mockToken = 'mocktoken' const mockEnv = 'stage' const mockOrg = 'mockorg' From 7570a62d0df24cebe247970e3999c2045144b4de Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Mon, 5 May 2025 17:10:14 +0800 Subject: [PATCH 12/21] remove non-logged in use case --- src/commands/app/deploy.js | 28 +-- src/commands/app/undeploy.js | 48 ++--- src/lib/app-helper.js | 25 --- src/lib/audit-logger.js | 78 ++++---- test/commands/app/deploy.test.js | 143 ++++++--------- test/commands/app/undeploy.test.js | 18 +- test/commands/lib/app-helper.test.js | 63 ------- test/commands/lib/audit-logger.test.js | 77 ++++---- test/data-mocks/config-loader.js | 38 ---- test/lib/audit-logger.test.js | 237 ------------------------- 10 files changed, 194 insertions(+), 561 deletions(-) delete mode 100644 test/lib/audit-logger.test.js diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index 852dc8bb3..77cf819ff 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -62,18 +62,20 @@ class Deploy extends BuildCommand { runtimeNamespace: aioConfig?.runtime?.namespace } - try { - // send audit log at start (don't wait for deployment to finish) - await sendAppDeployAuditLog({ - accessToken: cliDetails?.accessToken, - cliCommandFlags: flags, - appInfo, - env: cliDetails.env - }) - } catch (error) { - if (flags.verbose) { - this.warn('Error: Audit Log Service Error: Failed to send audit log event for deployment.') - this.warn(error.message) + if (cliDetails?.accessToken) { + try { + // send audit log at start (don't wait for deployment to finish) + await sendAppDeployAuditLog({ + accessToken: cliDetails?.accessToken, + cliCommandFlags: flags, + appInfo, + env: cliDetails.env + }) + } catch (error) { + if (flags.verbose) { + this.warn('Error: Audit Log Service Error: Failed to send audit log event for deployment.') + this.warn(error.message) + } } } @@ -125,7 +127,7 @@ class Deploy extends BuildCommand { const v = setRuntimeApiHostAndAuthHandler(values[i]) await this.deploySingleConfig(k, v, flags, spinner) - if (v.app.hasFrontend && flags['web-assets']) { + if (cliDetails?.accessToken && v.app.hasFrontend && flags['web-assets']) { const opItems = getFilesCountWithExtension(v.web.distProd) try { // only send logs in case of web-assets deployment diff --git a/src/commands/app/undeploy.js b/src/commands/app/undeploy.js index 72b55e686..1e5968a62 100644 --- a/src/commands/app/undeploy.js +++ b/src/commands/app/undeploy.js @@ -60,18 +60,20 @@ class Undeploy extends BaseCommand { runtimeNamespace: aioConfig?.runtime?.namespace } - try { - // send audit log at start (don't wait for deployment to finish) - await sendAppUndeployAuditLog({ - accessToken: cliDetails?.accessToken, - cliCommandFlags: flags, - appInfo, - env: cliDetails.env - }) - } catch (error) { - if (flags.verbose) { - this.warn('Error: Audit Log Service Error: Failed to send audit log event for deployment.') - this.warn(error.message) + if (cliDetails?.accessToken) { + try { + // send audit log at start (don't wait for deployment to finish) + await sendAppUndeployAuditLog({ + accessToken: cliDetails?.accessToken, + cliCommandFlags: flags, + appInfo, + env: cliDetails.env + }) + } catch (error) { + if (flags.verbose) { + this.warn('Error: Audit Log Service Error: Failed to send audit log event for deployment.') + this.warn(error.message) + } } } @@ -81,16 +83,18 @@ class Undeploy extends BaseCommand { const v = process.env.IS_DEPLOY_SERVICE_ENABLED === 'true' ? setRuntimeApiHostAndAuthHandler(values[i]) : values[i] await this.undeployOneExt(k, v, flags, spinner) - // send logs for case of web-assets undeployment - try { - await sendAppAssetsUndeployedAuditLog({ - accessToken: cliDetails?.accessToken, - cliCommandFlags: flags, - appInfo, - env: cliDetails.env - }) - } catch (error) { - this.warn('Warning: Audit Log Service Error: Failed to send audit log event for un-deployment.') + if (cliDetails?.accessToken) { + // send logs for case of web-assets undeployment + try { + await sendAppAssetsUndeployedAuditLog({ + accessToken: cliDetails?.accessToken, + cliCommandFlags: flags, + appInfo, + env: cliDetails.env + }) + } catch (error) { + this.warn('Warning: Audit Log Service Error: Failed to send audit log event for un-deployment.') + } } } diff --git a/src/lib/app-helper.js b/src/lib/app-helper.js index 9b170e33b..4b236e900 100644 --- a/src/lib/app-helper.js +++ b/src/lib/app-helper.js @@ -534,32 +534,7 @@ function getFilesCountWithExtension (directory) { return log } -/** - * Parses the namespace string into the appropriate components: - * (development, orgId, appName, workspace) - * - * @param {string} namespace the namespace - * @returns {object | null} the path components, or null if no match - */ -function parseNamespaceString (namespace) { - const namespaceLC = namespace.toLowerCase() - const regex = /^(development-)?(\d+)-([a-zA-Z0-9]+)(-([a-zA-Z0-9]+))?$/ - - const match = namespaceLC.match(regex) - if (match) { - return { - development: !!match[1], - orgId: match[2], - appName: match[3], - workspace: match[5] // if empty, it's basically prod - } - } - - return null -} - module.exports = { - parseNamespaceString, getObjectValue, getObjectProp, createWebExportFilter, diff --git a/src/lib/audit-logger.js b/src/lib/audit-logger.js index e324cf279..6626ccd8c 100644 --- a/src/lib/audit-logger.js +++ b/src/lib/audit-logger.js @@ -9,7 +9,6 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ const fetch = require('node-fetch') -const { parseNamespaceString } = require('./app-helper') const OPERATIONS = { AB_APP_DEPLOY: 'ab_app_deploy', @@ -18,14 +17,9 @@ const OPERATIONS = { AB_APP_ASSETS_UNDEPLOYED: 'ab_app_assets_undeployed' } -const AUDIT_SERVICE_ENDPOINTS2 = { - stage: 'https://adp-auditlog-service-stage.adobeioruntime.net/api/v1/web/audit-log-api/event-post', - prod: 'https://adp-auditlog-service-prod.adobeioruntime.net/api/v1/web/audit-log-api/event-post' -} - const AUDIT_SERVICE_ENDPOINTS = { - stage: 'http://127.0.0.1:3000/audit-log-api/event-post', - prod: 'http://127.0.0.1:3000/audit-log-api/event-post' + stage: process.env.AUDIT_SERVICE_ENDPOINT_STAGE ?? 'https://adp-auditlog-service-stage.adobeioruntime.net/api/v1/web/audit-log-api/event-post', + prod: process.env.AUDIT_SERVICE_ENDPOINT_PROD ?? 'https://adp-auditlog-service-prod.adobeioruntime.net/api/v1/web/audit-log-api/event-post' } /** @@ -33,7 +27,6 @@ const AUDIT_SERVICE_ENDPOINTS = { * @property {string} name - Application name * @property {string} version - Application version * @property {object} project - Project details containing org and workspace information - * @property {string} [runtimeNamespace] - Optional runtime namespace (for non-logged in use case) */ /** @@ -59,6 +52,31 @@ const AUDIT_SERVICE_ENDPOINTS = { * @property {string} operation - Operation type: 'ab_app_deploy', 'ab_app_undeploy', 'ab_app_assets_deployed', or 'ab_app_assets_undeployed' */ +/** + * Checks for environment variable overrides of audit service endpoints and logs warnings if found. + * + * This function checks for the following environment variables: + * - AUDIT_SERVICE_ENDPOINT_STAGE: Override for the stage environment endpoint + * - AUDIT_SERVICE_ENDPOINT_PROD: Override for the production environment endpoint + * + * If any of these variables are set, a warning will be logged to the console indicating + * which variables are being overridden and their values. + * + * @function checkOverrides + * @returns {void} + */ +function checkOverrides () { + const toCheck = ['AUDIT_SERVICE_ENDPOINT_STAGE', 'AUDIT_SERVICE_ENDPOINT_PROD'] + const overrides = toCheck.filter((toCheck) => process.env[toCheck]) + + if (overrides.length > 0) { + console.warn('Audit Service overrides detected:') + overrides.forEach((override) => { + console.warn(` ${override}: ${process.env[override]}`) + }) + } +} + /** * Publish audit log events to audit service * @@ -67,6 +85,8 @@ const AUDIT_SERVICE_ENDPOINTS = { * @throws {Error} If the audit log request fails */ async function publishAuditLogs ({ accessToken, logEvent, env = 'prod' }) { + checkOverrides() + const url = AUDIT_SERVICE_ENDPOINTS[env] ?? AUDIT_SERVICE_ENDPOINTS.prod const payload = { event: logEvent @@ -74,7 +94,7 @@ async function publishAuditLogs ({ accessToken, logEvent, env = 'prod' }) { const options = { method: 'POST', headers: { - Authorization: accessToken ? `Bearer ${accessToken}` : '', + Authorization: `Bearer ${accessToken}`, 'Content-type': 'application/json' }, body: JSON.stringify(payload) @@ -91,33 +111,27 @@ async function publishAuditLogs ({ accessToken, logEvent, env = 'prod' }) { * * @param {GetAuditLogEventParams} params - Parameters object containing CLI flags, operation type, and app info * @returns {object} Log event object containing audit log details - * @throws {Error} If project or runtimeNamespace is missing, or if operation is invalid + * @throws {Error} If project is missing, or if operation is invalid */ function getAuditLogEvent ({ cliCommandFlags, operation, appInfo }) { - const { project, runtimeNamespace } = appInfo + const { project } = appInfo - if (!project && !runtimeNamespace) { - throw new Error('Either project or runtimeNamespace is required') + if (!project) { + throw new Error('Project is required') } - let orgId, projectId, workspaceId, workspaceName - - if (project) { // logged in use case - if (!project.org) { - throw new Error('Project org is required') - } - if (!project.workspace) { - throw new Error('Project workspace is required') - } - orgId = project.org.id - projectId = project.id - workspaceId = project.workspace.id - workspaceName = project.workspace.name - } else { // non-logged in use case - const parsedNamespace = parseNamespaceString(runtimeNamespace) - workspaceName = parsedNamespace?.workspace ?? 'Production' + if (!project.org) { + throw new Error('Project org is required') + } + if (!project.workspace) { + throw new Error('Project workspace is required') } + const orgId = project.org.id + const projectId = project.id + const workspaceId = project.workspace.id + const workspaceName = project.workspace.name + let logStrMsg switch (operation) { case OPERATIONS.AB_APP_DEPLOY: @@ -137,7 +151,6 @@ function getAuditLogEvent ({ cliCommandFlags, operation, appInfo }) { } const logEvent = { - runtimeNamespace, orgId, projectId, workspaceId, @@ -211,5 +224,6 @@ module.exports = { sendAppDeployAuditLog, sendAppUndeployAuditLog, sendAppAssetsDeployedAuditLog, - sendAppAssetsUndeployedAuditLog + sendAppAssetsUndeployedAuditLog, + checkOverrides } diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index 2831cff70..cfaa9e826 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -67,16 +67,19 @@ const createAppConfig = (aioConfig = {}, appFixtureName = 'legacy-app') => { return appConfig } -const mockExtRegExcShellPayload = () => { +const mockExtRegExcShellPayload = ({ excShellView = true } = {}) => { const payload = { - endpoints: { - 'dx/excshell/1': { - view: [ - { metadata: {} } - ] - } + endpoints: {} + } + + if (excShellView) { + payload.endpoints['dx/excshell/1'] = { + view: [ + { metadata: {} } + ] } } + helpers.buildExtensionPointPayloadWoMetadata.mockReturnValueOnce(payload) mockLibConsoleCLI.updateExtensionPointsWithoutOverwrites.mockReturnValueOnce(payload) mockLibConsoleCLI.updateExtensionPoints.mockReturnValueOnce(payload) @@ -601,7 +604,6 @@ describe('run', () => { [['--no-actions']], [['--no-log-forwarding-update', '--no-actions']] ])('no log forwarding update due to %s arg(s) specified', async (args) => { - const appConfig = createAppConfig(command.appConfig) command.argv = args await command.run() expect(command.error).toHaveBeenCalledTimes(0) @@ -1109,7 +1111,7 @@ describe('run', () => { version: '1.0.0' } }) - mockExtRegExcShellPayload() + mockExtRegExcShellPayload({ excShellView: false }) await command.run() expect(mockLibConsoleCLI.getProject).toHaveBeenCalledTimes(1) @@ -1416,85 +1418,46 @@ describe('run', () => { }) }) - // test('Do not send audit logs for successful app deploy, if case of no token', async () => { - // const mockToken = null - // const mockEnv = 'stage' - // const mockOrg = 'mockorg' - // const mockProject = 'mockproject' - // const mockWorkspaceId = 'mockworkspaceid' - // const mockWorkspaceName = 'mockworkspacename' - - // helpers.getCliInfo.mockResolvedValueOnce({ - // accessToken: mockToken, - // env: mockEnv - // }) - - // const fullConfig = { - // aio: { - // project: { - // id: mockProject, - // org: { - // id: mockOrg - // }, - // workspace: { - // id: mockWorkspaceId, - // name: mockWorkspaceName - // } - // } - // }, - // packagejson: { - // name: 'test-app', - // version: '1.0.0' - // } - // } - // command.getFullConfig = jest.fn().mockReturnValue(fullConfig) - // command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) - - // await command.run() - // expect(command.error).toHaveBeenCalledTimes(0) - // expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) - // expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) - // expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(1) - // expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledWith({ - // accessToken: mockToken, - // appInfo: { - // name: 'test-app', - // version: '1.0.0', - // runtimeNamespace: undefined, - // project: { - // id: mockProject, - // org: { - // id: mockOrg - // }, - // workspace: { - // id: mockWorkspaceId, - // name: mockWorkspaceName - // } - // } - // }, - // cliCommandFlags: { - // actions: true, - // build: true, - // 'content-hash': true, - // 'force-build': true, - // 'force-deploy': false, - // 'force-events': false, - // 'force-publish': false, - // 'log-forwarding-update': true, - // open: false, - // publish: false, - // 'web-assets': true, - // 'web-optimize': false - // }, - // env: mockEnv, - // opItems: [ - // '3 Javascript file(s)', - // '2 CSS file(s)', - // '5 image(s)', - // '1 HTML page(s)' - // ] - // }) - // }) + test('Do not send audit logs for successful app deploy, if case of no token', async () => { + const mockToken = null + const mockEnv = 'stage' + const mockOrg = 'mockorg' + const mockProject = 'mockproject' + const mockWorkspaceId = 'mockworkspaceid' + const mockWorkspaceName = 'mockworkspacename' + + helpers.getCliInfo.mockResolvedValueOnce({ + accessToken: mockToken, + env: mockEnv + }) + + const fullConfig = { + aio: { + project: { + id: mockProject, + org: { + id: mockOrg + }, + workspace: { + id: mockWorkspaceId, + name: mockWorkspaceName + } + } + }, + packagejson: { + name: 'test-app', + version: '1.0.0' + } + } + command.getFullConfig = jest.fn().mockReturnValue(fullConfig) + command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) + + await command.run() + expect(command.error).toHaveBeenCalledTimes(0) + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) + expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) + expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(0) + }) test('Send audit logs for successful app deploy + web assets', async () => { const mockToken = 'mocktoken' @@ -1578,7 +1541,7 @@ describe('run', () => { }) }) - test('Should deploy successfully even if Audit log service is unavailable', async () => { + test('Should deploy successfully even if (app deploy) Audit log service is unavailable (--verbose)', async () => { const mockToken = 'mocktoken' const mockEnv = 'stage' const mockOrg = 'mockorg' @@ -1631,7 +1594,7 @@ describe('run', () => { expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) }) - test('Should deploy successfully even if Audit log service is unavailable', async () => { + test('Should deploy successfully even if (app assets deploy)Audit log service is unavailable', async () => { const mockToken = 'mocktoken' const mockEnv = 'stage' const mockOrg = 'mockorg' diff --git a/test/commands/app/undeploy.test.js b/test/commands/app/undeploy.test.js index aeb2999d9..bc9deb56d 100644 --- a/test/commands/app/undeploy.test.js +++ b/test/commands/app/undeploy.test.js @@ -13,7 +13,7 @@ governing permissions and limitations under the License. const TheCommand = require('../../../src/commands/app/undeploy') const BaseCommand = require('../../../src/BaseCommand') const dataMocks = require('../../data-mocks/config-loader') -const { cloneDeep } = require('lodash') +const cloneDeep = require('lodash.clonedeep') jest.mock('../../../src/lib/app-helper.js') const helpers = require('../../../src/lib/app-helper.js') @@ -260,16 +260,16 @@ describe('run', () => { command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig()) command.warn = jest.fn() command.argv = ['-v'] - + // Mock audit logger to throw an error auditLogger.sendAppUndeployAuditLog.mockRejectedValueOnce(new Error('Audit log error')) - + await command.run() - + // Verify error was logged with verbose flag expect(command.warn).toHaveBeenCalledWith('Error: Audit Log Service Error: Failed to send audit log event for deployment.') expect(command.warn).toHaveBeenCalledWith('Audit log error') - + // Verify deployment still continues expect(mockRuntimeLib.undeployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(1) @@ -278,15 +278,15 @@ describe('run', () => { test('should handle audit log error without verbose flag', async () => { command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig()) command.warn = jest.fn() - + // Mock audit logger to throw an error auditLogger.sendAppUndeployAuditLog.mockRejectedValueOnce(new Error('Audit log error')) - + await command.run() - + // Verify error was not logged without verbose flag expect(command.warn).not.toHaveBeenCalled() - + // Verify deployment still continues expect(mockRuntimeLib.undeployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(1) diff --git a/test/commands/lib/app-helper.test.js b/test/commands/lib/app-helper.test.js index 7a3487cc0..f7fd53ec6 100644 --- a/test/commands/lib/app-helper.test.js +++ b/test/commands/lib/app-helper.test.js @@ -873,66 +873,3 @@ describe('getFilesCountWithExtension', () => { ]) }) }) - -describe('parseNamespaceString', () => { - test('should parse development namespace correctly', () => { - const namespace = 'development-12345-myapp-dev' - const result = appHelper.parseNamespaceString(namespace) - expect(result).toEqual({ - development: true, - orgId: '12345', - appName: 'myapp', - workspace: 'dev' - }) - }) - - test('should parse production namespace correctly', () => { - const namespace = '12345-myapp-prod' - const result = appHelper.parseNamespaceString(namespace) - expect(result).toEqual({ - development: false, - orgId: '12345', - appName: 'myapp', - workspace: 'prod' - }) - }) - - test('should parse namespace without workspace correctly', () => { - const namespace = '12345-myapp' - const result = appHelper.parseNamespaceString(namespace) - expect(result).toEqual({ - development: false, - orgId: '12345', - appName: 'myapp', - workspace: undefined - }) - }) - - test('should handle case insensitive namespace', () => { - const namespace = 'DEVELOPMENT-12345-MYAPP-DEV' - const result = appHelper.parseNamespaceString(namespace) - expect(result).toEqual({ - development: true, - orgId: '12345', - appName: 'myapp', - workspace: 'dev' - }) - }) - - test('should return null for invalid namespace format', () => { - const invalidNamespaces = [ - 'invalid', - '12345', - 'myapp', - 'development', - 'development-', - '12345-', - '12345-myapp-' - ] - - invalidNamespaces.forEach(namespace => { - const result = appHelper.parseNamespaceString(namespace) - expect(result).toBeNull() - }) - }) -}) diff --git a/test/commands/lib/audit-logger.test.js b/test/commands/lib/audit-logger.test.js index 6de9ee858..7495162d7 100644 --- a/test/commands/lib/audit-logger.test.js +++ b/test/commands/lib/audit-logger.test.js @@ -17,7 +17,8 @@ const { sendAppDeployAuditLog, sendAppUndeployAuditLog, sendAppAssetsDeployedAuditLog, - sendAppAssetsUndeployedAuditLog + sendAppAssetsUndeployedAuditLog, + checkOverrides } = require('../../../src/lib/audit-logger') jest.mock('node-fetch') @@ -39,8 +40,47 @@ describe('audit-logger', () => { } const mockCliFlags = { flag1: 'value1' } - beforeEach(() => { - jest.clearAllMocks() + describe('checkOverrides', () => { + const originalEnv = process.env + + beforeEach(() => { + jest.resetModules() // Clears any cached modules + process.env = { ...originalEnv } // Copies the original environment variables + jest.spyOn(console, 'warn').mockImplementation() + }) + + afterEach(() => { + process.env = originalEnv // Restores the original environment variables + console.warn.mockRestore() + }) + + it('should not log warnings when no environment variables are set', () => { + checkOverrides() + expect(console.warn).not.toHaveBeenCalled() + }) + + it('should log warning when only stage endpoint is overridden', () => { + process.env.AUDIT_SERVICE_ENDPOINT_STAGE = 'https://custom-stage-endpoint.com' + checkOverrides() + expect(console.warn).toHaveBeenCalledWith('Audit Service overrides detected:') + expect(console.warn).toHaveBeenCalledWith(' AUDIT_SERVICE_ENDPOINT_STAGE: https://custom-stage-endpoint.com') + }) + + it('should log warning when only prod endpoint is overridden', () => { + process.env.AUDIT_SERVICE_ENDPOINT_PROD = 'https://custom-prod-endpoint.com' + checkOverrides() + expect(console.warn).toHaveBeenCalledWith('Audit Service overrides detected:') + expect(console.warn).toHaveBeenCalledWith(' AUDIT_SERVICE_ENDPOINT_PROD: https://custom-prod-endpoint.com') + }) + + it('should log warnings when both endpoints are overridden', () => { + process.env.AUDIT_SERVICE_ENDPOINT_STAGE = 'https://custom-stage-endpoint.com' + process.env.AUDIT_SERVICE_ENDPOINT_PROD = 'https://custom-prod-endpoint.com' + checkOverrides() + expect(console.warn).toHaveBeenCalledWith('Audit Service overrides detected:') + expect(console.warn).toHaveBeenCalledWith(' AUDIT_SERVICE_ENDPOINT_STAGE: https://custom-stage-endpoint.com') + expect(console.warn).toHaveBeenCalledWith(' AUDIT_SERVICE_ENDPOINT_PROD: https://custom-prod-endpoint.com') + }) }) describe('getAuditLogEvent', () => { @@ -68,33 +108,6 @@ describe('audit-logger', () => { }) }) - it('should create a valid audit log event for app deploy (non-logged in)', () => { - const nonLoggedInAppInfo = { - name: 'test-app', - version: '1.0.0', - runtimeNamespace: 'test-namespace' - } - const event = getAuditLogEvent({ - cliCommandFlags: mockCliFlags, - operation: OPERATIONS.AB_APP_DEPLOY, - appInfo: nonLoggedInAppInfo - }) - - expect(event).toEqual({ - runtimeNamespace: 'test-namespace', - workspaceName: 'Production', - operation: OPERATIONS.AB_APP_DEPLOY, - appName: 'test-app', - appVersion: '1.0.0', - objectName: 'test-app', - timestamp: expect.any(Number), - data: { - cliCommandFlags: mockCliFlags, - opDetailsStr: expect.stringContaining('Starting deployment for the App Builder application') - } - }) - }) - it('should create a valid audit log event for app undeploy', () => { const event = getAuditLogEvent({ cliCommandFlags: mockCliFlags, @@ -119,7 +132,7 @@ describe('audit-logger', () => { }) }) - it('should throw error if neither project nor runtimeNamespace is provided', () => { + it('should throw error if project is not provided', () => { const invalidAppInfo = { name: 'test-app', version: '1.0.0' @@ -128,7 +141,7 @@ describe('audit-logger', () => { cliCommandFlags: mockCliFlags, operation: OPERATIONS.AB_APP_DEPLOY, appInfo: invalidAppInfo - })).toThrow('Either project or runtimeNamespace is required') + })).toThrow('Project is required') }) it('should throw error if project org is missing', () => { diff --git a/test/data-mocks/config-loader.js b/test/data-mocks/config-loader.js index 70274427a..970507d62 100644 --- a/test/data-mocks/config-loader.js +++ b/test/data-mocks/config-loader.js @@ -480,41 +480,3 @@ module.exports = (appFixtureName, mockedAIOConfig, rewriteMockConfig = {}) => { aio: mockedAIOConfig } } - -const buildExtensionPointPayloadWoMetadata = (deployConfigs) => { - const payload = { - endpoints: {} - } - - // Add excshell extension point if present - if (deployConfigs['dx/excshell/1']) { - payload.endpoints['dx/excshell/1'] = { - view: [{ - name: 'test-app', - url: 'https://adobeio-static.net/test-app/index.html', - metadata: { - name: 'test-app', - description: 'Test App', - version: '1.0.0' - } - }] - } - } - - // Add asset compute worker extension point if present - if (deployConfigs['dx/asset-compute/worker/1']) { - payload.endpoints['dx/asset-compute/worker/1'] = { - workerProcess: [] - } - } - - return payload -} - -const buildExcShellViewExtensionMetadata = async (libConsoleCLI, aioConfig) => { - return { - name: 'test-app', - description: 'Test App', - version: '1.0.0' - } -} diff --git a/test/lib/audit-logger.test.js b/test/lib/audit-logger.test.js deleted file mode 100644 index 2ca472967..000000000 --- a/test/lib/audit-logger.test.js +++ /dev/null @@ -1,237 +0,0 @@ -const fetch = require('node-fetch') -const { sendAppDeployAuditLog, sendAppUndeployAuditLog, sendAppAssetsDeployedAuditLog, sendAppAssetsUndeployedAuditLog } = require('../../src/lib/audit-logger') - -jest.mock('node-fetch') - -describe('audit-logger', () => { - const mockAccessToken = 'mock-token' - const mockEnv = 'stage' - const mockAppInfo = { - name: 'test-app', - version: '1.0.0', - project: { - id: 'project-1', - org: { - id: 'org-1' - }, - workspace: { - id: 'workspace-1', - name: 'test-workspace' - } - } - } - const mockCliCommandFlags = { - actions: true, - build: true, - 'web-assets': true - } - const mockOpItems = [ - '3 Javascript file(s)', - '2 CSS file(s)', - '5 image(s)', - '1 HTML page(s)' - ] - - beforeEach(() => { - jest.clearAllMocks() - fetch.mockResolvedValue({ - status: 200, - text: () => Promise.resolve('success') - }) - }) - - describe('sendAppDeployAuditLog', () => { - test('should send deploy audit log successfully', async () => { - await sendAppDeployAuditLog({ - accessToken: mockAccessToken, - cliCommandFlags: mockCliCommandFlags, - appInfo: mockAppInfo, - env: mockEnv - }) - - expect(fetch).toHaveBeenCalledWith( - expect.stringContaining('/audit-log-api/event-post'), - expect.objectContaining({ - method: 'POST', - headers: { - Authorization: `Bearer ${mockAccessToken}`, - 'Content-type': 'application/json' - }, - body: expect.stringContaining('ab_app_deploy') - }) - ) - }) - - test('should handle missing access token', async () => { - await sendAppDeployAuditLog({ - accessToken: null, - cliCommandFlags: mockCliCommandFlags, - appInfo: mockAppInfo, - env: mockEnv - }) - - expect(fetch).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: '' - }) - }) - ) - }) - - test('should throw error on failed request', async () => { - fetch.mockResolvedValue({ - status: 500, - text: () => Promise.resolve('Internal Server Error') - }) - - await expect(sendAppDeployAuditLog({ - accessToken: mockAccessToken, - cliCommandFlags: mockCliCommandFlags, - appInfo: mockAppInfo, - env: mockEnv - })).rejects.toThrow('Failed to send audit log') - }) - }) - - describe('sendAppUndeployAuditLog', () => { - test('should send undeploy audit log successfully', async () => { - await sendAppUndeployAuditLog({ - accessToken: mockAccessToken, - cliCommandFlags: mockCliCommandFlags, - appInfo: mockAppInfo, - env: mockEnv - }) - - expect(fetch).toHaveBeenCalledWith( - expect.stringContaining('/audit-log-api/event-post'), - expect.objectContaining({ - method: 'POST', - headers: { - Authorization: `Bearer ${mockAccessToken}`, - 'Content-type': 'application/json' - }, - body: expect.stringContaining('ab_app_undeploy') - }) - ) - }) - }) - - describe('sendAppAssetsDeployedAuditLog', () => { - test('should send assets deployed audit log successfully', async () => { - await sendAppAssetsDeployedAuditLog({ - accessToken: mockAccessToken, - cliCommandFlags: mockCliCommandFlags, - appInfo: mockAppInfo, - opItems: mockOpItems, - env: mockEnv - }) - - expect(fetch).toHaveBeenCalledWith( - expect.stringContaining('/audit-log-api/event-post'), - expect.objectContaining({ - method: 'POST', - headers: { - Authorization: `Bearer ${mockAccessToken}`, - 'Content-type': 'application/json' - }, - body: expect.stringContaining('ab_app_assets_deployed') - }) - ) - }) - - test('should include operation items in the log event', async () => { - await sendAppAssetsDeployedAuditLog({ - accessToken: mockAccessToken, - cliCommandFlags: mockCliCommandFlags, - appInfo: mockAppInfo, - opItems: mockOpItems, - env: mockEnv - }) - - const requestBody = JSON.parse(fetch.mock.calls[0][1].body) - expect(requestBody.event.data.opItems).toEqual(mockOpItems) - }) - }) - - describe('sendAppAssetsUndeployedAuditLog', () => { - test('should send assets undeployed audit log successfully', async () => { - await sendAppAssetsUndeployedAuditLog({ - accessToken: mockAccessToken, - cliCommandFlags: mockCliCommandFlags, - appInfo: mockAppInfo, - env: mockEnv - }) - - expect(fetch).toHaveBeenCalledWith( - expect.stringContaining('/audit-log-api/event-post'), - expect.objectContaining({ - method: 'POST', - headers: { - Authorization: `Bearer ${mockAccessToken}`, - 'Content-type': 'application/json' - }, - body: expect.stringContaining('ab_app_assets_undeployed') - }) - ) - }) - }) - - describe('error cases', () => { - test('should throw error when project and runtimeNamespace are missing', async () => { - const invalidAppInfo = { - name: 'test-app', - version: '1.0.0' - } - - await expect(sendAppDeployAuditLog({ - accessToken: mockAccessToken, - cliCommandFlags: mockCliCommandFlags, - appInfo: invalidAppInfo, - env: mockEnv - })).rejects.toThrow('Either project or runtimeNamespace is required') - }) - - test('should throw error when project org is missing', async () => { - const invalidAppInfo = { - name: 'test-app', - version: '1.0.0', - project: { - id: 'project-1', - workspace: { - id: 'workspace-1', - name: 'test-workspace' - } - } - } - - await expect(sendAppDeployAuditLog({ - accessToken: mockAccessToken, - cliCommandFlags: mockCliCommandFlags, - appInfo: invalidAppInfo, - env: mockEnv - })).rejects.toThrow('Project org is required') - }) - - test('should throw error when project workspace is missing', async () => { - const invalidAppInfo = { - name: 'test-app', - version: '1.0.0', - project: { - id: 'project-1', - org: { - id: 'org-1' - } - } - } - - await expect(sendAppDeployAuditLog({ - accessToken: mockAccessToken, - cliCommandFlags: mockCliCommandFlags, - appInfo: invalidAppInfo, - env: mockEnv - })).rejects.toThrow('Project workspace is required') - }) - }) -}) \ No newline at end of file From 469dcf08467fce40db5799384bdddd5f5521029a Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Mon, 5 May 2025 17:10:56 +0800 Subject: [PATCH 13/21] linter fixes --- src/lib/audit-logger.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/audit-logger.js b/src/lib/audit-logger.js index 6626ccd8c..78f5fd965 100644 --- a/src/lib/audit-logger.js +++ b/src/lib/audit-logger.js @@ -58,10 +58,10 @@ const AUDIT_SERVICE_ENDPOINTS = { * This function checks for the following environment variables: * - AUDIT_SERVICE_ENDPOINT_STAGE: Override for the stage environment endpoint * - AUDIT_SERVICE_ENDPOINT_PROD: Override for the production environment endpoint - * + * * If any of these variables are set, a warning will be logged to the console indicating * which variables are being overridden and their values. - * + * * @function checkOverrides * @returns {void} */ From 4846104c17d4b0bad25f8d6c61c47e0bdf4e0e36 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Mon, 5 May 2025 20:19:55 +0800 Subject: [PATCH 14/21] re-add send runtime namespace for audit log --- src/lib/audit-logger.js | 8 +++++++- test/commands/lib/audit-logger.test.js | 20 ++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/lib/audit-logger.js b/src/lib/audit-logger.js index 78f5fd965..fdf8a915d 100644 --- a/src/lib/audit-logger.js +++ b/src/lib/audit-logger.js @@ -27,6 +27,7 @@ const AUDIT_SERVICE_ENDPOINTS = { * @property {string} name - Application name * @property {string} version - Application version * @property {object} project - Project details containing org and workspace information + * @property {object} namespace - the runtime namespace */ /** @@ -114,7 +115,7 @@ async function publishAuditLogs ({ accessToken, logEvent, env = 'prod' }) { * @throws {Error} If project is missing, or if operation is invalid */ function getAuditLogEvent ({ cliCommandFlags, operation, appInfo }) { - const { project } = appInfo + const { project, runtimeNamespace } = appInfo if (!project) { throw new Error('Project is required') @@ -127,6 +128,10 @@ function getAuditLogEvent ({ cliCommandFlags, operation, appInfo }) { throw new Error('Project workspace is required') } + if (!runtimeNamespace) { + throw new Error('Runtime namespace is required') + } + const orgId = project.org.id const projectId = project.id const workspaceId = project.workspace.id @@ -160,6 +165,7 @@ function getAuditLogEvent ({ cliCommandFlags, operation, appInfo }) { appVersion: appInfo.version, objectName: appInfo.name, timestamp: new Date().valueOf(), + runtimeNamespace, data: { cliCommandFlags, opDetailsStr: logStrMsg diff --git a/test/commands/lib/audit-logger.test.js b/test/commands/lib/audit-logger.test.js index 7495162d7..99c4a0a05 100644 --- a/test/commands/lib/audit-logger.test.js +++ b/test/commands/lib/audit-logger.test.js @@ -36,7 +36,8 @@ describe('audit-logger', () => { const mockAppInfo = { name: 'test-app', version: '1.0.0', - project: mockProject + project: mockProject, + runtimeNamespace: 'fake-namespace' } const mockCliFlags = { flag1: 'value1' } @@ -84,7 +85,7 @@ describe('audit-logger', () => { }) describe('getAuditLogEvent', () => { - it('should create a valid audit log event for app deploy (logged in)', () => { + it('should create a valid audit log event for app deploy', () => { const event = getAuditLogEvent({ cliCommandFlags: mockCliFlags, operation: OPERATIONS.AB_APP_DEPLOY, @@ -101,6 +102,7 @@ describe('audit-logger', () => { appVersion: '1.0.0', objectName: 'test-app', timestamp: expect.any(Number), + runtimeNamespace: 'fake-namespace', data: { cliCommandFlags: mockCliFlags, opDetailsStr: expect.stringContaining('Starting deployment for the App Builder application') @@ -125,6 +127,7 @@ describe('audit-logger', () => { appVersion: '1.0.0', objectName: 'test-app', timestamp: expect.any(Number), + runtimeNamespace: 'fake-namespace', data: { cliCommandFlags: mockCliFlags, opDetailsStr: expect.stringContaining('Starting undeployment for the App Builder application') @@ -168,6 +171,19 @@ describe('audit-logger', () => { })).toThrow('Project workspace is required') }) + it('should throw error if runtime namespace is missing', () => { + const invalidAppInfo = { + ...mockAppInfo, + runtimeNamespace: null, + project: { ...mockProject } + } + expect(() => getAuditLogEvent({ + cliCommandFlags: mockCliFlags, + operation: OPERATIONS.AB_APP_DEPLOY, + appInfo: invalidAppInfo + })).toThrow('Runtime namespace is required') + }) + it('should throw error for invalid operation', () => { expect(() => getAuditLogEvent({ cliCommandFlags: mockCliFlags, From 56222fc7ab09ff1b0b3e86e507ce148a8b9b446a Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Sat, 10 May 2025 01:57:05 +0800 Subject: [PATCH 15/21] remove opDetailsStr (not needed), updated event property use --- src/lib/audit-logger.js | 28 ++++++-------------------- test/commands/lib/audit-logger.test.js | 14 ++++++------- 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/src/lib/audit-logger.js b/src/lib/audit-logger.js index fdf8a915d..21f7e04ac 100644 --- a/src/lib/audit-logger.js +++ b/src/lib/audit-logger.js @@ -132,43 +132,27 @@ function getAuditLogEvent ({ cliCommandFlags, operation, appInfo }) { throw new Error('Runtime namespace is required') } + if (!Object.values(OPERATIONS).find((op) => op === operation)) { + throw new Error(`Invalid operation: ${operation}`) + } const orgId = project.org.id const projectId = project.id const workspaceId = project.workspace.id const workspaceName = project.workspace.name - let logStrMsg - switch (operation) { - case OPERATIONS.AB_APP_DEPLOY: - logStrMsg = `Starting deployment for the App Builder application in workspace ${workspaceName}` - break - case OPERATIONS.AB_APP_UNDEPLOY: - logStrMsg = `Starting undeployment for the App Builder application in workspace ${workspaceName}` - break - case OPERATIONS.AB_APP_ASSETS_UNDEPLOYED: - logStrMsg = `All static assets for the App Builder application in workspace: ${workspaceName} were successfully undeployed from the CDN` - break - case OPERATIONS.AB_APP_ASSETS_DEPLOYED: - logStrMsg = `All static assets for the App Builder application in workspace: ${workspaceName} were successfully deployed to the CDN.\n Files deployed - ` - break - default: - throw new Error(`Invalid operation: ${operation}`) - } - const logEvent = { orgId, projectId, workspaceId, workspaceName, operation, - appName: appInfo.name, - appVersion: appInfo.version, + objectRef: appInfo.name, + objectRev: appInfo.version, objectName: appInfo.name, timestamp: new Date().valueOf(), runtimeNamespace, data: { - cliCommandFlags, - opDetailsStr: logStrMsg + cliCommandFlags } } return logEvent diff --git a/test/commands/lib/audit-logger.test.js b/test/commands/lib/audit-logger.test.js index 99c4a0a05..2cadc33b3 100644 --- a/test/commands/lib/audit-logger.test.js +++ b/test/commands/lib/audit-logger.test.js @@ -98,14 +98,13 @@ describe('audit-logger', () => { workspaceId: 'fake-workspace-id', workspaceName: 'fake-workspace', operation: OPERATIONS.AB_APP_DEPLOY, - appName: 'test-app', - appVersion: '1.0.0', + objectRef: 'test-app', + objectRev: '1.0.0', objectName: 'test-app', timestamp: expect.any(Number), runtimeNamespace: 'fake-namespace', data: { - cliCommandFlags: mockCliFlags, - opDetailsStr: expect.stringContaining('Starting deployment for the App Builder application') + cliCommandFlags: mockCliFlags } }) }) @@ -123,14 +122,13 @@ describe('audit-logger', () => { workspaceId: 'fake-workspace-id', workspaceName: 'fake-workspace', operation: OPERATIONS.AB_APP_UNDEPLOY, - appName: 'test-app', - appVersion: '1.0.0', + objectRef: 'test-app', + objectRev: '1.0.0', objectName: 'test-app', timestamp: expect.any(Number), runtimeNamespace: 'fake-namespace', data: { - cliCommandFlags: mockCliFlags, - opDetailsStr: expect.stringContaining('Starting undeployment for the App Builder application') + cliCommandFlags: mockCliFlags } }) }) From e0956560c88f25c1193d5bed5af6a3b33adbc856 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Sat, 10 May 2025 03:16:42 +0800 Subject: [PATCH 16/21] two log forwarding commands were not using auth_handler settings, fixed --- src/commands/app/config/get/log-forwarding.js | 8 +++++++- src/commands/app/config/get/log-forwarding/errors.js | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/commands/app/config/get/log-forwarding.js b/src/commands/app/config/get/log-forwarding.js index 9ad7ab62e..4e962df2a 100644 --- a/src/commands/app/config/get/log-forwarding.js +++ b/src/commands/app/config/get/log-forwarding.js @@ -11,10 +11,16 @@ governing permissions and limitations under the License. const BaseCommand = require('../../../../BaseCommand') const LogForwarding = require('../../../../lib/log-forwarding') +const { setRuntimeApiHostAndAuthHandler } = require('../../../../lib/auth-helper') class LogForwardingCommand extends BaseCommand { async run () { - const lf = await LogForwarding.init((await this.getFullConfig()).aio) + let aioConfig = (await this.getFullConfig()).aio + // TODO: remove this check once the deploy service is enabled by default + if (process.env.IS_DEPLOY_SERVICE_ENABLED === 'true') { + aioConfig = setRuntimeApiHostAndAuthHandler(aioConfig) + } + const lf = await LogForwarding.init(aioConfig) const localConfig = lf.getLocalConfig() const serverConfig = await lf.getServerConfig() diff --git a/src/commands/app/config/get/log-forwarding/errors.js b/src/commands/app/config/get/log-forwarding/errors.js index bc0bfba43..327941fcc 100644 --- a/src/commands/app/config/get/log-forwarding/errors.js +++ b/src/commands/app/config/get/log-forwarding/errors.js @@ -12,6 +12,7 @@ governing permissions and limitations under the License. const BaseCommand = require('../../../../../BaseCommand') const rtLib = require('@adobe/aio-lib-runtime') const ora = require('ora') +const { setRuntimeApiHostAndAuthHandler } = require('../../../../../lib/auth-helper') class ErrorsCommand extends BaseCommand { async run () { @@ -30,7 +31,13 @@ class ErrorsCommand extends BaseCommand { } async getLogForwarding () { - const runtimeConfig = (await this.getFullConfig()).aio.runtime + let aioConfig = (await this.getFullConfig()).aio + // TODO: remove this check once the deploy service is enabled by default + if (process.env.IS_DEPLOY_SERVICE_ENABLED === 'true') { + aioConfig = setRuntimeApiHostAndAuthHandler(aioConfig) + } + + const runtimeConfig = aioConfig.runtime rtLib.utils.checkOpenWhiskCredentials({ ow: runtimeConfig }) const rt = await rtLib.init({ ...runtimeConfig, From 6998bd38a7e9ec1190b562d6607aa0bd1918604f Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Sat, 10 May 2025 03:17:04 +0800 Subject: [PATCH 17/21] temp: use branch version of aio-lib-runtime --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4cf2d2d48..be709f0f0 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@adobe/aio-lib-core-networking": "^5", "@adobe/aio-lib-env": "^3", "@adobe/aio-lib-ims": "^7", - "@adobe/aio-lib-runtime": "^7.1.0", + "@adobe/aio-lib-runtime": "github:adobe/aio-lib-runtime#story/ACNA-3770", "@adobe/aio-lib-templates": "^3", "@adobe/aio-lib-web": "^7", "@adobe/generator-aio-app": "github:adobe/generator-aio-app#master", From 4e4facc1d7075e65f53028d261f4c055a9acf758 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Mon, 12 May 2025 13:36:23 +0800 Subject: [PATCH 18/21] fix coverage --- .../app/config/get/log-forwarding.test.js | 28 ++++ .../config/get/log-forwarding/errors.test.js | 131 +++++++++--------- 2 files changed, 94 insertions(+), 65 deletions(-) diff --git a/test/commands/app/config/get/log-forwarding.test.js b/test/commands/app/config/get/log-forwarding.test.js index 07105f207..af5532a82 100644 --- a/test/commands/app/config/get/log-forwarding.test.js +++ b/test/commands/app/config/get/log-forwarding.test.js @@ -13,6 +13,7 @@ governing permissions and limitations under the License. const { stdout } = require('stdout-stderr') const TheCommand = require('../../../../../src/commands/app/config/get/log-forwarding.js') const LogForwarding = require('../../../../../src/lib/log-forwarding') +const { setRuntimeApiHostAndAuthHandler } = require('../../../../../src/lib/auth-helper') jest.mock('../../../../../src/lib/log-forwarding', () => { const orig = jest.requireActual('../../../../../src/lib/log-forwarding') @@ -22,6 +23,10 @@ jest.mock('../../../../../src/lib/log-forwarding', () => { } }) +jest.mock('../../../../../src/lib/auth-helper', () => ({ + setRuntimeApiHostAndAuthHandler: jest.fn(config => config) +})) + let command, lf beforeEach(async () => { command = new TheCommand([]) @@ -189,3 +194,26 @@ test('failed to get log forwarding settings', async () => { lf.getServerConfig.mockRejectedValue(new Error('mocked error')) await expect(command.run()).rejects.toThrow('mocked error') }) + +test('get log forwarding settings with deploy service enabled', async () => { + process.env.IS_DEPLOY_SERVICE_ENABLED = 'true' + const localConfig = new LogForwarding.LogForwardingConfig() + const serverConfig = new LogForwarding.LogForwardingConfig() + + lf.getLocalConfig.mockReturnValue(localConfig) + lf.getServerConfig.mockResolvedValue(serverConfig) + + await command.run() + expect(setRuntimeApiHostAndAuthHandler).toHaveBeenCalledWith(command.appConfig.aio) + expect(LogForwarding.init).toHaveBeenCalledWith(command.appConfig.aio) + + delete process.env.IS_DEPLOY_SERVICE_ENABLED +}) + +test('command aliases are set correctly', () => { + expect(TheCommand.aliases).toEqual(['app:config:get:log-forwarding', 'app:config:get:lf']) +}) + +test('command description is set correctly', () => { + expect(TheCommand.description).toBe('Get log forwarding destination configuration') +}) diff --git a/test/commands/app/config/get/log-forwarding/errors.test.js b/test/commands/app/config/get/log-forwarding/errors.test.js index be3b17ce2..47ad8d3ec 100644 --- a/test/commands/app/config/get/log-forwarding/errors.test.js +++ b/test/commands/app/config/get/log-forwarding/errors.test.js @@ -10,13 +10,23 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const TheCommand = require('../../../../../../src/commands/app/config/get/log-forwarding/errors') -const RuntimeLib = require('@adobe/aio-lib-runtime') -const ora = require('ora') +const { stdout } = require('stdout-stderr') +const TheCommand = require('../../../../../../src/commands/app/config/get/log-forwarding/errors.js') +const rtLib = require('@adobe/aio-lib-runtime') +const { setRuntimeApiHostAndAuthHandler } = require('../../../../../../src/lib/auth-helper') -jest.mock('ora') +jest.mock('@adobe/aio-lib-runtime', () => ({ + init: jest.fn(), + utils: { + checkOpenWhiskCredentials: jest.fn() + } +})) + +jest.mock('../../../../../../src/lib/auth-helper', () => ({ + setRuntimeApiHostAndAuthHandler: jest.fn(config => config) +})) -let command, rtLib, spinner +let command, logForwarding beforeEach(async () => { command = new TheCommand([]) command.appConfig = { @@ -30,79 +40,70 @@ beforeEach(async () => { } } } - rtLib = await RuntimeLib.init({ apihost: 'https://adobeioruntime.net', api_key: 'fake:auth' }) - RuntimeLib.utils.checkOpenWhiskCredentials = jest.fn() - rtLib.logForwarding.getErrors = jest.fn() - spinner = ora() + logForwarding = { + getErrors: jest.fn() + } + rtLib.init.mockResolvedValue({ logForwarding }) }) -test('app:config:get:log-forwarding:errors command', async () => { - return new Promise(resolve => { - rtLib.logForwarding.getErrors.mockResolvedValue({ - configured_forwarder: 'destination', - errors: [ - 'error 1', - 'error 2' - ] - }) - - return command.run() - .then(() => { - expect(spinner.succeed) - .toHaveBeenCalledWith("Log forwarding errors for the last configured destination 'destination':\nerror 1\nerror 2") - resolve() - }) +test('get log forwarding errors with errors', async () => { + const errors = ['Error 1', 'Error 2'] + logForwarding.getErrors.mockResolvedValue({ + errors, + configured_forwarder: 'test-destination' }) + + await command.run() + expect(stdout.output).toContain('Log forwarding errors for the last configured destination \'test-destination\':') + expect(stdout.output).toContain('Error 1') + expect(stdout.output).toContain('Error 2') }) -test('app:config:get:log-forwarding:errors command - no destination returned from the server', async () => { - return new Promise(resolve => { - rtLib.logForwarding.getErrors.mockResolvedValue({ - errors: [ - 'error 1', - 'error 2' - ] - }) - - return command.run() - .then(() => { - expect(spinner.succeed).toHaveBeenCalledWith('Log forwarding errors:\nerror 1\nerror 2') - resolve() - }) +test('get log forwarding errors without errors', async () => { + logForwarding.getErrors.mockResolvedValue({ + errors: [], + configured_forwarder: 'test-destination' }) + + await command.run() + expect(stdout.output).toContain('No log forwarding errors for the last configured destination \'test-destination\'') }) -test('app:config:get:log-forwarding:errors command - no errors', async () => { - return new Promise(resolve => { - rtLib.logForwarding.getErrors.mockResolvedValue({ - configured_forwarder: 'destination', - errors: [] - }) - - return command.run() - .then(() => { - expect(spinner.succeed) - .toHaveBeenCalledWith("No log forwarding errors for the last configured destination 'destination'") - resolve() - }) +test('get log forwarding errors without configured forwarder', async () => { + logForwarding.getErrors.mockResolvedValue({ + errors: ['Error 1'] }) + + await command.run() + expect(stdout.output).toContain('Log forwarding errors:') + expect(stdout.output).toContain('Error 1') }) -test('app:config:get:log-forwarding:errors command - no errors and no destination returned from the server', async () => { - return new Promise(resolve => { - rtLib.logForwarding.getErrors.mockResolvedValue({ - errors: [] - }) - - return command.run() - .then(() => { - expect(spinner.succeed).toHaveBeenCalledWith('No log forwarding errors') - resolve() - }) +test('get log forwarding errors with deploy service enabled', async () => { + process.env.IS_DEPLOY_SERVICE_ENABLED = 'true' + logForwarding.getErrors.mockResolvedValue({ + errors: [] }) + + await command.run() + expect(setRuntimeApiHostAndAuthHandler).toHaveBeenCalledWith(command.appConfig.aio) + expect(rtLib.init).toHaveBeenCalledWith({ + ...command.appConfig.aio.runtime, + api_key: command.appConfig.aio.runtime.auth + }) + + delete process.env.IS_DEPLOY_SERVICE_ENABLED }) -test('app:config:get:log-forwarding:errors command - failed response from server', async () => { - rtLib.logForwarding.getErrors.mockRejectedValue(new Error('mocked error')) +test('failed to get log forwarding errors', async () => { + logForwarding.getErrors.mockRejectedValue(new Error('mocked error')) await expect(command.run()).rejects.toThrow('mocked error') }) + +test('command aliases are set correctly', () => { + expect(TheCommand.aliases).toEqual(['app:config:get:log-forwarding:errors', 'app:config:get:lf:errors']) +}) + +test('command description is set correctly', () => { + expect(TheCommand.description).toBe('Get log forwarding errors') +}) From af1f5924cc0c3c55f82453ec418a034d6fd764a9 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Mon, 12 May 2025 13:36:42 +0800 Subject: [PATCH 19/21] remove repo branch specific dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index be709f0f0..aa1b1f88c 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "@adobe/aio-lib-core-networking": "^5", "@adobe/aio-lib-env": "^3", "@adobe/aio-lib-ims": "^7", - "@adobe/aio-lib-runtime": "github:adobe/aio-lib-runtime#story/ACNA-3770", + "@adobe/aio-lib-runtime": "^7.1.2", "@adobe/aio-lib-templates": "^3", "@adobe/aio-lib-web": "^7", - "@adobe/generator-aio-app": "github:adobe/generator-aio-app#master", + "@adobe/generator-aio-app": "^9", "@adobe/generator-app-common-lib": "^2", "@adobe/inquirer-table-checkbox": "^2", "@oclif/core": "^2.11.6", From 0a56091399d799d0c8856c8e53782aed2224f74f Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Mon, 12 May 2025 14:00:18 +0800 Subject: [PATCH 20/21] fix merge issues --- src/lib/audit-logger.js | 1 - test/commands/lib/audit-logger.test.js | 19 +++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/lib/audit-logger.js b/src/lib/audit-logger.js index 21f7e04ac..078f7fd75 100644 --- a/src/lib/audit-logger.js +++ b/src/lib/audit-logger.js @@ -8,7 +8,6 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -const fetch = require('node-fetch') const OPERATIONS = { AB_APP_DEPLOY: 'ab_app_deploy', diff --git a/test/commands/lib/audit-logger.test.js b/test/commands/lib/audit-logger.test.js index 2cadc33b3..c0b2c4298 100644 --- a/test/commands/lib/audit-logger.test.js +++ b/test/commands/lib/audit-logger.test.js @@ -9,7 +9,6 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const fetch = require('node-fetch') const { OPERATIONS, AUDIT_SERVICE_ENDPOINTS, @@ -21,7 +20,9 @@ const { checkOverrides } = require('../../../src/lib/audit-logger') -jest.mock('node-fetch') +beforeEach(() => { + setFetchMock(true, 200, {}) +}) describe('audit-logger', () => { const mockAccessToken = 'fake-token' @@ -193,7 +194,7 @@ describe('audit-logger', () => { describe('sendAppDeployAuditLog', () => { it('should send app deploy audit log successfully', async () => { - fetch.mockResolvedValueOnce({ status: 200 }) + setFetchMock(true, 200, {}) await sendAppDeployAuditLog({ accessToken: mockAccessToken, @@ -216,7 +217,7 @@ describe('audit-logger', () => { describe('sendAppUndeployAuditLog', () => { it('should send app undeploy audit log successfully', async () => { - fetch.mockResolvedValueOnce({ status: 200 }) + setFetchMock(true, 200, {}) await sendAppUndeployAuditLog({ accessToken: mockAccessToken, @@ -239,7 +240,8 @@ describe('audit-logger', () => { describe('sendAppAssetsDeployedAuditLog', () => { it('should send app assets deployed audit log successfully', async () => { - fetch.mockResolvedValueOnce({ status: 200 }) + setFetchMock(true, 200, {}) + const mockOpItems = ['file1.js', 'file2.css'] await sendAppAssetsDeployedAuditLog({ @@ -265,7 +267,7 @@ describe('audit-logger', () => { describe('sendAppAssetsUndeployedAuditLog', () => { it('should send app assets undeployed audit log successfully', async () => { - fetch.mockResolvedValueOnce({ status: 200 }) + setFetchMock(true, 200, {}) await sendAppAssetsUndeployedAuditLog({ accessToken: mockAccessToken, @@ -288,10 +290,7 @@ describe('audit-logger', () => { describe('error handling', () => { it('should throw error when audit service returns non-200 status', async () => { - fetch.mockResolvedValueOnce({ - status: 500, - text: () => Promise.resolve('Internal Server Error') - }) + setFetchMock(true, 500, 'Internal Server Error') await expect(sendAppDeployAuditLog({ accessToken: mockAccessToken, From a2e12bc2c8f8c9a88822549708667922463f9bf5 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah Date: Wed, 14 May 2025 14:55:51 +0800 Subject: [PATCH 21/21] fix error when updating log forwarding during deploy (when it is defined in the config) --- src/commands/app/deploy.js | 9 +++++++-- test/commands/app/deploy.test.js | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index 77cf819ff..2ae040542 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -84,7 +84,12 @@ class Deploy extends BuildCommand { if (aioConfig?.project?.workspace && flags['log-forwarding-update'] && flags.actions) { spinner.start('Updating log forwarding configuration') try { - const lf = await LogForwarding.init(aioConfig) + let lfConfig = aioConfig + if (process.env.IS_DEPLOY_SERVICE_ENABLED === 'true') { + lfConfig = setRuntimeApiHostAndAuthHandler(aioConfig) + } + + const lf = await LogForwarding.init(lfConfig) if (lf.isLocalConfigChanged()) { const lfConfig = lf.getLocalConfigWithSecrets() if (lfConfig.isDefined()) { @@ -124,7 +129,7 @@ class Deploy extends BuildCommand { // - break into smaller pieces deploy, allowing to first deploy all actions then all web assets for (let i = 0; i < keys.length; ++i) { const k = keys[i] - const v = setRuntimeApiHostAndAuthHandler(values[i]) + const v = process.env.IS_DEPLOY_SERVICE_ENABLED === 'true' ? setRuntimeApiHostAndAuthHandler(values[i]) : values[i] await this.deploySingleConfig(k, v, flags, spinner) if (cliDetails?.accessToken && v.app.hasFrontend && flags['web-assets']) { diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index cfaa9e826..e6ac8887a 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -1296,7 +1296,7 @@ describe('run', () => { expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledTimes(1) - expect(authHelper.setRuntimeApiHostAndAuthHandler).toHaveBeenCalledTimes(1) + expect(authHelper.setRuntimeApiHostAndAuthHandler).toHaveBeenCalledTimes(2) // once in logforwarding, once when deploying expect(auditLogger.sendAppAssetsDeployedAuditLog).toHaveBeenCalledWith({ accessToken: mockToken, appInfo: {