Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/commands/app/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ const BaseCommand = require('../../BaseCommand')
const BuildCommand = require('./build')
const webLib = require('@adobe/aio-lib-web')
const { Flags } = require('@oclif/core')
const { runInProcess, buildExtensionPointPayloadWoMetadata, buildExcShellViewExtensionMetadata, getFilesCountWithExtension } = require('../../lib/app-helper')
const {
rewriteActionUrlInEntities, runInProcess,
buildExtensionPointPayloadWoMetadata, buildExcShellViewExtensionMetadata,
getFilesCountWithExtension
} = require('../../lib/app-helper')
const rtLib = require('@adobe/aio-lib-runtime')
const LogForwarding = require('../../lib/log-forwarding')
const { sendAppAssetsDeployedAuditLog, sendAppDeployAuditLog } = require('../../lib/audit-logger')
Expand Down Expand Up @@ -128,7 +132,7 @@ class Deploy extends BuildCommand {
const k = keys[i]
const v = setRuntimeApiHostAndAuthHandler(values[i])

await this.deploySingleConfig(k, v, flags, spinner)
await this.deploySingleConfig({ name: k, config: v, originalConfig: values[i], flags, spinner })
if (cliDetails?.accessToken && v.app.hasFrontend && flags['web-assets']) {
const opItems = getFilesCountWithExtension(v.web.distProd)
try {
Expand Down Expand Up @@ -167,7 +171,7 @@ class Deploy extends BuildCommand {
this.log(chalk.green(chalk.bold('Successful deployment 🏄')))
}

async deploySingleConfig (name, config, flags, spinner) {
async deploySingleConfig ({ name, config, originalConfig, flags, spinner }) {
const onProgress = !flags.verbose
? info => {
spinner.text = info
Expand Down Expand Up @@ -267,7 +271,8 @@ class Deploy extends BuildCommand {

// log deployed resources
if (deployedRuntimeEntities.actions && deployedRuntimeEntities.actions.length > 0) {
await logActions({ entities: deployedRuntimeEntities, log: (...rest) => this.log(chalk.bold(chalk.blue(...rest))) })
const entities = await rewriteActionUrlInEntities({ entities: deployedRuntimeEntities, config: originalConfig })
await logActions({ entities, log: (...rest) => this.log(chalk.bold(chalk.blue(...rest))) })
}

// TODO urls should depend on extension point, exc shell only for exc shell extension point - use a post-app-deploy hook ?
Expand Down
42 changes: 42 additions & 0 deletions src/lib/app-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,49 @@ function getFilesCountWithExtension (directory) {
return log
}

/**
* Rewrites action URLs in deployed runtime entities using URLs from the manifest configuration.
*
* This function takes deployed runtime entities and updates the URL property of each action
* with the corresponding URL from the runtime manifest configuration. It creates a deep copy
* of the entities to avoid mutating the original object.
*
* @param {object} params - Parameters object
* @param {object} params.entities - The deployed runtime entities object
* @param {object} params.config - The application configuration object containing runtime manifest
* @returns {Promise<object>} A promise that resolves to a deep copy of the entities object with updated action URLs
* @example
* const entities = {
* actions: [
* { name: 'my-action', url: 'old-url' },
* { name: 'another-action', url: 'another-old-url' }
* ]
* }
* const config = {
* actions: { devRemote: false },
* // ... other config properties
* }
*
* const rewrittenEntities = await rewriteActionUrlInEntities({ entities, config })
* // rewrittenEntities.actions will have updated URLs from the manifest
*/
async function rewriteActionUrlInEntities ({ entities, config }) {
const actionUrlsFromManifest = RuntimeLib.utils.getActionUrls(config, config.actions.devRemote)
const rewrittenEntities = structuredClone(entities)

rewrittenEntities.actions = rewrittenEntities.actions?.map(action => {
const retAction = structuredClone(action)
const url = actionUrlsFromManifest[action.name]
if (url) {
retAction.url = url
}
return retAction
})
return rewrittenEntities
}

module.exports = {
rewriteActionUrlInEntities,
getObjectValue,
getObjectProp,
createWebExportFilter,
Expand Down
3 changes: 2 additions & 1 deletion src/lib/auth-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ const bearerAuthHandler = {
}
}

const setRuntimeApiHostAndAuthHandler = (config) => {
const setRuntimeApiHostAndAuthHandler = (_config) => {
const config = structuredClone(_config)
const aioConfig = (config && 'runtime' in config) ? config : null
if (aioConfig) {
const apiEndpoint = process.env.AIO_DEPLOY_SERVICE_URL ?? defaultDeployServiceUrl
Expand Down
8 changes: 7 additions & 1 deletion test/commands/app/config/get/log-forwarding.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ test('get log forwarding settings (expect init to be passed a config)', async ()
lf.getServerConfig.mockResolvedValue(serverConfig)

await command.run()
expect(LogForwarding.init).toHaveBeenCalledWith(command.appConfig.aio)
// config should be deploy service settings
const modifiedConfig = structuredClone(command.appConfig.aio)
modifiedConfig.runtime.apihost = 'https://deploy-service.app-builder.adp.adobe.io/runtime'
modifiedConfig.runtime.auth_handler = {
getAuthHeader: expect.any(Function)
}
expect(LogForwarding.init).toHaveBeenCalledWith(modifiedConfig)
})

test('get log forwarding settings (local and server are the same)', async () => {
Expand Down
6 changes: 6 additions & 0 deletions test/commands/app/deploy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ beforeEach(() => {
helpers.buildExtensionPointPayloadWoMetadata.mockReset()
helpers.buildExcShellViewExtensionMetadata.mockReset()
helpers.createWebExportFilter.mockReset()
helpers.rewriteActionUrlInEntities.mockReset()
mockLogForwarding.isLocalConfigChanged.mockReset()
mockLogForwarding.getLocalConfigWithSecrets.mockReset()
mockLogForwarding.updateServerConfig.mockReset()
Expand All @@ -183,6 +184,11 @@ beforeEach(() => {
'1 HTML page(s)'
]
})

helpers.rewriteActionUrlInEntities.mockImplementation(async ({ entities }) => {
return entities
})

authHelper.setRuntimeApiHostAndAuthHandler.mockImplementation((aioConfig) => aioConfig)
authHelper.getAccessToken.mockImplementation(() => {
return {
Expand Down
220 changes: 220 additions & 0 deletions test/commands/lib/app-helper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -847,3 +847,223 @@ describe('getFilesCountWithExtension', () => {
])
})
})

describe('rewriteActionUrlInEntities', () => {
const RuntimeLib = require('@adobe/aio-lib-runtime')

beforeEach(() => {
RuntimeLib.utils.getActionUrls = jest.fn()
})

afterEach(() => {
jest.clearAllMocks()
})

test('should rewrite action URLs with URLs from manifest', async () => {
const entities = {
actions: [
{ name: 'action1', url: 'old-url-1' },
{ name: 'action2', url: 'old-url-2' }
]
}
const config = {
actions: { devRemote: false }
}
const mockActionUrls = {
action1: 'https://example.com/api/v1/web/action1',
action2: 'https://example.com/api/v1/web/action2'
}

RuntimeLib.utils.getActionUrls.mockReturnValue(mockActionUrls)

const result = await appHelper.rewriteActionUrlInEntities({ entities, config })

expect(RuntimeLib.utils.getActionUrls).toHaveBeenCalledWith(config, false)
expect(result.actions).toEqual([
{ name: 'action1', url: 'https://example.com/api/v1/web/action1' },
{ name: 'action2', url: 'https://example.com/api/v1/web/action2' }
])
})

test('should use devRemote flag when calling getActionUrls', async () => {
const entities = { actions: [] }
const config = {
actions: { devRemote: true }
}

RuntimeLib.utils.getActionUrls.mockReturnValue({})

await appHelper.rewriteActionUrlInEntities({ entities, config })

expect(RuntimeLib.utils.getActionUrls).toHaveBeenCalledWith(config, true)
})

test('should preserve original action when no URL found in manifest', async () => {
const entities = {
actions: [
{ name: 'action1', url: 'original-url' },
{ name: 'action2', url: 'another-original-url' }
]
}
const config = {
actions: { devRemote: false }
}
const mockActionUrls = {
action1: 'https://example.com/api/v1/web/action1'
// action2 is missing from manifest URLs
}

RuntimeLib.utils.getActionUrls.mockReturnValue(mockActionUrls)

const result = await appHelper.rewriteActionUrlInEntities({ entities, config })

expect(result.actions).toEqual([
{ name: 'action1', url: 'https://example.com/api/v1/web/action1' },
{ name: 'action2', url: 'another-original-url' } // Original URL preserved
])
})

test('should handle entities with no actions array', async () => {
const entities = {}
const config = {
actions: { devRemote: false }
}

RuntimeLib.utils.getActionUrls.mockReturnValue({})

const result = await appHelper.rewriteActionUrlInEntities({ entities, config })

expect(result.actions).toBeUndefined()
expect(RuntimeLib.utils.getActionUrls).toHaveBeenCalledWith(config, false)
})

test('should handle entities with undefined actions', async () => {
const entities = { actions: undefined }
const config = {
actions: { devRemote: false }
}

RuntimeLib.utils.getActionUrls.mockReturnValue({})

const result = await appHelper.rewriteActionUrlInEntities({ entities, config })

expect(result.actions).toBeUndefined()
})

test('should handle empty actions array', async () => {
const entities = { actions: [] }
const config = {
actions: { devRemote: false }
}

RuntimeLib.utils.getActionUrls.mockReturnValue({})

const result = await appHelper.rewriteActionUrlInEntities({ entities, config })

expect(result.actions).toEqual([])
})

test('should create deep copy and not mutate original entities', async () => {
const originalEntities = {
actions: [
{ name: 'action1', url: 'original-url', otherProp: 'value' }
],
otherProp: 'test'
}
const config = {
actions: { devRemote: false }
}
const mockActionUrls = {
action1: 'https://example.com/api/v1/web/action1'
}

RuntimeLib.utils.getActionUrls.mockReturnValue(mockActionUrls)

const result = await appHelper.rewriteActionUrlInEntities({ entities: originalEntities, config })

// Original should be unchanged
expect(originalEntities.actions[0].url).toBe('original-url')
expect(originalEntities.otherProp).toBe('test')

// Result should have the new URL
expect(result.actions[0].url).toBe('https://example.com/api/v1/web/action1')
expect(result.actions[0].otherProp).toBe('value')
expect(result.otherProp).toBe('test')

// Should be different objects
expect(result).not.toBe(originalEntities)
expect(result.actions).not.toBe(originalEntities.actions)
expect(result.actions[0]).not.toBe(originalEntities.actions[0])
})

test('should preserve all other action properties', async () => {
const entities = {
actions: [
{
name: 'action1',
url: 'old-url',
version: '1.0.0',
annotations: { 'web-export': true },
parameters: { key: 'value' },
exec: { kind: 'nodejs:18' }
}
]
}
const config = {
actions: { devRemote: false }
}
const mockActionUrls = {
action1: 'https://example.com/api/v1/web/action1'
}

RuntimeLib.utils.getActionUrls.mockReturnValue(mockActionUrls)

const result = await appHelper.rewriteActionUrlInEntities({ entities, config })

expect(result.actions[0]).toEqual({
name: 'action1',
url: 'https://example.com/api/v1/web/action1',
version: '1.0.0',
annotations: { 'web-export': true },
parameters: { key: 'value' },
exec: { kind: 'nodejs:18' }
})
})

test('should handle empty action URLs from manifest', async () => {
const entities = {
actions: [
{ name: 'action1', url: 'original-url' }
]
}
const config = {
actions: { devRemote: false }
}

RuntimeLib.utils.getActionUrls.mockReturnValue({}) // Empty object

const result = await appHelper.rewriteActionUrlInEntities({ entities, config })

expect(result.actions[0].url).toBe('original-url') // Should preserve original
})

test('should handle null/undefined URL from manifest', async () => {
const entities = {
actions: [
{ name: 'action1', url: 'original-url' }
]
}
const config = {
actions: { devRemote: false }
}
const mockActionUrls = {
action1: null // Null URL
}

RuntimeLib.utils.getActionUrls.mockReturnValue(mockActionUrls)

const result = await appHelper.rewriteActionUrlInEntities({ entities, config })

expect(result.actions[0].url).toBe('original-url') // Should preserve original when URL is falsy
})
})