diff --git a/src/hooks/post-deploy-event-reg.js b/src/hooks/post-deploy-event-reg.js index b000c68..33dde88 100644 --- a/src/hooks/post-deploy-event-reg.js +++ b/src/hooks/post-deploy-event-reg.js @@ -12,6 +12,6 @@ const { WEBHOOK, deployRegistration } = require('./utils/hook-utils') -module.exports = async function ({ appConfig }) { - await deployRegistration({ appConfig }, WEBHOOK, 'post-deploy-event-reg') +module.exports = async function ({ appConfig, force }) { + await deployRegistration({ appConfig }, WEBHOOK, 'post-deploy-event-reg', force) } diff --git a/src/hooks/utils/hook-utils.js b/src/hooks/utils/hook-utils.js index 8e145e8..2df950e 100644 --- a/src/hooks/utils/hook-utils.js +++ b/src/hooks/utils/hook-utils.js @@ -35,7 +35,7 @@ function getDeliveryType (registration) { } /** - * + * @private * @param {object} registration - registration object * @param {object} providerMetadataToProviderIdMapping - mapping of provider metadata to provider id * @returns {Array} of events of interest {provider_id, event_code} @@ -58,7 +58,7 @@ function getEventsOfInterestForRegistration (registration, } /** - * + * @private * @param {object} projectConfig - project config object * @returns {object} Object containing orgId, X_API_KEY, eventsClient */ @@ -75,6 +75,7 @@ async function initEventsSdk (projectConfig) { } /** + * @private * @returns {object} Object containing mapping of provider metadata to provider id */ function getProviderMetadataToProviderIdMapping () { @@ -91,20 +92,30 @@ function getProviderMetadataToProviderIdMapping () { } /** + * @private * @param {object} eventRegistrations - registrations from the .aio config file * @returns {object} Object containing mapping of registration name to registration object */ -function getRegistrationsFromAioConfig (eventRegistrations) { +function getRegistrationNameToRegistrationsMap (eventRegistrations) { const registrationNameToRegistrations = {} - if (eventRegistrations) { - for (const registration of eventRegistrations) { - registrationNameToRegistrations[registration.name] = registration - } + for (const registration of eventRegistrations) { + registrationNameToRegistrations[registration.name] = registration } return registrationNameToRegistrations } /** + * @private + * @param {Array.} workspaceRegistrationNames Registration names from the Console workspace + * @param {Array.} appConfigRegistrationNames Registration names defined in the app.config.yaml file + * @returns {Array.} Registrations that are part of the workspace, but not part of the app.config.yaml + */ +function getWorkspaceRegistrationsToBeDeleted (workspaceRegistrationNames, appConfigRegistrationNames) { + return workspaceRegistrationNames.filter(wsRegistration => !appConfigRegistrationNames.includes(wsRegistration)) +} + +/** + * @private * @param {object} body - Registration Create/Update Model * @param {object} eventsSDK - eventsSDK object containing eventsClient and orgId * @param {object} existingRegistration - existing registration obtained from .aio config if exists @@ -114,14 +125,39 @@ async function createOrUpdateRegistration (body, eventsSDK, existingRegistration if (existingRegistration) { const response = await eventsSDK.eventsClient.updateRegistration(eventsSDK.orgId, project.id, project.workspace.id, existingRegistration.registration_id, body) - console.log('Updated registration:' + JSON.stringify(response)) + console.log('Updated registration with name:' + response.name + ' and id:' + response.registration_id) } else { const response = await eventsSDK.eventsClient.createRegistration(eventsSDK.orgId, project.id, project.workspace.id, body) - console.log('Created registration:' + JSON.stringify(response)) + console.log('Created registration:' + response.name + ' and id:' + response.registration_id) } } +/** + * @private + * @param {object} eventsSDK - eventsSDK object containing eventsClient and orgId + * @param {object} existingRegistration - existing registration obtained from .aio config if exists + * @param {object} project - project details from .aio config file + */ +async function deleteRegistration (eventsSDK, existingRegistration, project) { + await eventsSDK.eventsClient.deleteRegistration(eventsSDK.orgId, project.id, project.workspace.id, + existingRegistration.registration_id) + console.log('Deleted registration with name:' + existingRegistration.name + ' and id:' + existingRegistration.registration_id) +} + +/** + * @private + * @param {object} eventsSDK - eventsSDK object containing eventsClient and orgId + * @param {object} project - project details from .aio config file + * @returns {object} Object containing all registrations for the workspace + */ +async function getAllRegistrationsForWorkspace (eventsSDK, project) { + const registrationsForWorkspace = await eventsSDK.eventsClient.getAllRegistrationsForWorkspace(eventsSDK.orgId, project.id, + project.workspace.id) + if (!registrationsForWorkspace) { return {} } + return getRegistrationNameToRegistrationsMap(registrationsForWorkspace._embedded.registrations) +} + /** * @param {object} appConfigRoot - Root object containing events and project details * @param {object} appConfigRoot.appConfig - Object containing events and project details @@ -129,8 +165,9 @@ async function createOrUpdateRegistration (body, eventsSDK, existingRegistration * @param {object} appConfigRoot.appConfig.events - Events registrations that are part of the app.config.yaml file * @param {string} expectedDeliveryType - Delivery type based on the hook that is calling. Expected delivery type can be webhook or journal * @param {string} hookType - pre-deploy-event-reg or post-deploy-event-reg hook values + * @param {boolean} forceEventsFlag - determines if registrations that are part of the workspace but not part of the app.config.yaml will be deleted or not */ -async function deployRegistration ({ appConfig: { events, project } }, expectedDeliveryType, hookType) { +async function deployRegistration ({ appConfig: { events, project } }, expectedDeliveryType, hookType, forceEventsFlag) { if (!project) { throw new Error( `No project found, skipping event registration in ${hookType} hook`) @@ -145,30 +182,39 @@ async function deployRegistration ({ appConfig: { events, project } }, expectedD throw new Error( `Events SDK could not be initialised correctly. Skipping event registration in ${hookType} hook`) } - const registrations = events.registrations + const registrationsFromConfig = events.registrations const providerMetadataToProviderIdMapping = getProviderMetadataToProviderIdMapping() - let existingRegistrations - if (project.workspace.details.events) { - existingRegistrations = getRegistrationsFromAioConfig( - project.workspace.details.events.registrations) - } - - for (const registrationName in registrations) { - const deliveryType = getDeliveryType(registrations[registrationName]) + const registrationsFromWorkspace = await getAllRegistrationsForWorkspace(eventsSDK, project) + for (const registrationName in registrationsFromConfig) { + const deliveryType = getDeliveryType(registrationsFromConfig[registrationName]) if (deliveryType === expectedDeliveryType) { const body = { name: registrationName, client_id: eventsSDK.X_API_KEY, - description: registrations[registrationName].description, + description: registrationsFromConfig[registrationName].description, delivery_type: deliveryType, + runtime_action: registrationsFromConfig[registrationName].runtime_action, events_of_interest: getEventsOfInterestForRegistration( - registrations[registrationName], providerMetadataToProviderIdMapping) + registrationsFromConfig[registrationName], providerMetadataToProviderIdMapping) } try { let existingRegistration - if (existingRegistrations && existingRegistrations[registrationName]) { existingRegistration = existingRegistrations[registrationName] } - await createOrUpdateRegistration(body, eventsSDK, - existingRegistration, project) + if (registrationsFromWorkspace && registrationsFromWorkspace[registrationName]) { existingRegistration = registrationsFromWorkspace[registrationName] } + await createOrUpdateRegistration(body, eventsSDK, existingRegistration, project) + } catch (e) { + throw new Error( + e + '\ncode:' + e.code + '\nDetails:' + JSON.stringify( + e.sdkDetails)) + } + } + } + + if (forceEventsFlag) { + const registrationsToDeleted = getWorkspaceRegistrationsToBeDeleted(Object.keys(registrationsFromWorkspace), Object.keys(registrationsFromConfig)) + console.log('The following registrations will be deleted: ', registrationsToDeleted) + for (const registrationName of registrationsToDeleted) { + try { + await deleteRegistration(eventsSDK, registrationsFromWorkspace[registrationName], project) } catch (e) { throw new Error( e + '\ncode:' + e.code + '\nDetails:' + JSON.stringify( diff --git a/test/__fixtures__/registration/list.json b/test/__fixtures__/registration/list.json index 23c172b..c1e752f 100644 --- a/test/__fixtures__/registration/list.json +++ b/test/__fixtures__/registration/list.json @@ -14,7 +14,7 @@ } }, "id": 11111, - "name": "bowling 1", + "name": "Event Registration 1", "description": "let me know when we can go play bowling!", "client_id": "1234654902189324798", "webhook_url": "https://send-me-a-bowling-event.com/right-now", @@ -48,7 +48,7 @@ } }, "id": 22222, - "name": "table tenis 2", + "name": "Event Registration 2", "description": "registration for table tennis events", "client_id": "1234654902189324798", "webhook_url": "https://send-me-a-table-tennis-event.com/please", diff --git a/test/__fixtures__/registration/list.txt b/test/__fixtures__/registration/list.txt index 1a59639..ea3ba9a 100644 --- a/test/__fixtures__/registration/list.txt +++ b/test/__fixtures__/registration/list.txt @@ -1,4 +1,4 @@ ID NAME ENABLED DELIVERY_TYPE WEBHOOK_STATUS ───────────────────────────────────── ──────────────────────── ───────── ───────────── ────────────── - REGID1 bowling 1 true webhook verified - REGID2 table tenis 2 true webhook verified + REGID1 Event Registration 1 true webhook verified + REGID2 Event Registration 2 true webhook verified diff --git a/test/__fixtures__/registration/list.yml b/test/__fixtures__/registration/list.yml index 6999c7a..414c9c5 100644 --- a/test/__fixtures__/registration/list.yml +++ b/test/__fixtures__/registration/list.yml @@ -11,7 +11,7 @@ _embedded: href: >- https://api.adobe.io/events/consumerOrgId/projectId/workspaceId/registrations/REGID1 id: 11111 - name: bowling 1 + name: Event Registration 1 description: let me know when we can go play bowling! client_id: '1234654902189324798' webhook_url: 'https://send-me-a-bowling-event.com/right-now' @@ -39,7 +39,7 @@ _embedded: href: >- https://api.adobe.io/events/consumerOrgId/projectId/workspaceId/registrations/REGID2 id: 22222 - name: table tenis 2 + name: Event Registration 2 description: registration for table tennis events client_id: '1234654902189324798' webhook_url: 'https://send-me-a-table-tennis-event.com/please' diff --git a/test/hooks/post-deploy-event-reg.test.js b/test/hooks/post-deploy-event-reg.test.js index e26745d..db92869 100644 --- a/test/hooks/post-deploy-event-reg.test.js +++ b/test/hooks/post-deploy-event-reg.test.js @@ -14,7 +14,9 @@ jest.mock('@adobe/aio-lib-events') const eventsSdk = require('@adobe/aio-lib-events') const mockEventsSdkInstance = { createRegistration: jest.fn(), - updateRegistration: jest.fn() + updateRegistration: jest.fn(), + getAllRegistrationsForWorkspace: jest.fn(), + deleteRegistration: jest.fn() } jest.mock('@adobe/aio-lib-ims') const { getToken } = require('@adobe/aio-lib-ims') @@ -32,6 +34,10 @@ describe('post deploy event registration hook interfaces', () => { eventsSdk.init.mockResolvedValue(mockEventsSdkInstance) }) + afterEach(() => { + eventsSdk.init.mockRestore() + }) + test('hook interface', async () => { const hook = require('../../src/hooks/post-deploy-event-reg') expect(typeof hook).toBe('function') @@ -126,6 +132,7 @@ describe('post deploy event registration hook interfaces', () => { getToken.mockReturnValue('accessToken') const events = mock.data.sampleEvents events.registrations['Event Registration 1'].delivery_type = 'webhook' + mockEventsSdkInstance.getAllRegistrationsForWorkspace.mockResolvedValue(mock.data.getAllWebhookRegistrationsWithEmptyResponse) mockEventsSdkInstance.createRegistration.mockReturnValue(mock.data.createWebhookRegistrationResponse) const projectWithEmptyEvents = mock.data.sampleProjectWithoutEvents projectWithEmptyEvents.workspace.details.events = {} @@ -141,6 +148,7 @@ describe('post deploy event registration hook interfaces', () => { expect(typeof hook).toBe('function') process.env = mock.data.dotEnv getToken.mockReturnValue('accessToken') + mockEventsSdkInstance.getAllRegistrationsForWorkspace.mockResolvedValue(mock.data.getAllWebhookRegistrationsResponse) mockEventsSdkInstance.updateRegistration.mockRejectedValueOnce(JSON.stringify({ code: 500, errorDetails: { @@ -149,7 +157,7 @@ describe('post deploy event registration hook interfaces', () => { })) await expect(hook({ appConfig: { project: mock.data.sampleProject, events: mock.data.sampleEvents } })).rejects.toThrowError() expect(mockEventsSdkInstance.updateRegistration).toBeCalledTimes(1) - expect(mockEventsSdkInstance.updateRegistration).toHaveBeenCalledWith(CONSUMER_ID, PROJECT_ID, WORKSPACE_ID, 'registrationId1', + expect(mockEventsSdkInstance.updateRegistration).toHaveBeenCalledWith(CONSUMER_ID, PROJECT_ID, WORKSPACE_ID, 'REGID1', mock.data.hookDecodedEventRegistration1 ) }) @@ -159,11 +167,83 @@ describe('post deploy event registration hook interfaces', () => { expect(typeof hook).toBe('function') process.env = mock.data.dotEnv getToken.mockReturnValue('accessToken') + mockEventsSdkInstance.getAllRegistrationsForWorkspace.mockResolvedValue(mock.data.getAllWebhookRegistrationsResponse) mockEventsSdkInstance.updateRegistration.mockReturnValue(mock.data.createWebhookRegistrationResponse) await expect(hook({ appConfig: { project: mock.data.sampleProject, events: mock.data.sampleEvents } })).resolves.not.toThrowError() expect(mockEventsSdkInstance.updateRegistration).toBeCalledTimes(1) - expect(mockEventsSdkInstance.updateRegistration).toHaveBeenCalledWith(CONSUMER_ID, PROJECT_ID, WORKSPACE_ID, 'registrationId1', + expect(mockEventsSdkInstance.updateRegistration).toHaveBeenCalledWith(CONSUMER_ID, PROJECT_ID, WORKSPACE_ID, 'REGID1', mock.data.hookDecodedEventRegistration1 ) }) + + test('successfully delete registrations not part of the config', async () => { + const hook = require('../../src/hooks/post-deploy-event-reg') + expect(typeof hook).toBe('function') + process.env = mock.data.dotEnv + getToken.mockReturnValue('accessToken') + const events = { + registrations: { + 'Event Registration 1': mock.data.sampleEvents.registrations['Event Registration 1'] + } + } + mockEventsSdkInstance.getAllRegistrationsForWorkspace.mockResolvedValue(mock.data.getAllWebhookRegistrationsResponse) + mockEventsSdkInstance.updateRegistration.mockReturnValue(mock.data.createWebhookRegistrationResponse) + const projectWithEmptyEvents = mock.data.sampleProjectWithoutEvents + await expect(hook({ appConfig: { project: projectWithEmptyEvents, events }, force: true })).resolves.not.toThrowError() + expect(mockEventsSdkInstance.updateRegistration).toBeCalledTimes(1) + expect(mockEventsSdkInstance.deleteRegistration).toHaveBeenCalledWith(CONSUMER_ID, PROJECT_ID, WORKSPACE_ID, 'REGID2') + }) + + test('test delete registrations not part of the config, with no registrations to delete', async () => { + const hook = require('../../src/hooks/post-deploy-event-reg') + expect(typeof hook).toBe('function') + process.env = mock.data.dotEnv + getToken.mockReturnValue('accessToken') + mockEventsSdkInstance.getAllRegistrationsForWorkspace.mockResolvedValue(mock.data.getAllWebhookRegistrationsResponse) + mockEventsSdkInstance.updateRegistration.mockReturnValue(mock.data.createWebhookRegistrationResponse) + await expect(hook({ appConfig: { project: mock.data.sampleProject, events: mock.data.sampleEvents }, force: true })).resolves.not.toThrowError() + expect(mockEventsSdkInstance.updateRegistration).toBeCalledTimes(1) + expect(mockEventsSdkInstance.updateRegistration).toHaveBeenCalledWith(CONSUMER_ID, PROJECT_ID, WORKSPACE_ID, 'REGID1', + mock.data.hookDecodedEventRegistration1) + expect(mockEventsSdkInstance.deleteRegistration).toHaveBeenCalledTimes(0) + }) + + test('test delete registrations not part of the config, with no registrations in workspace', async () => { + const hook = require('../../src/hooks/post-deploy-event-reg') + expect(typeof hook).toBe('function') + process.env = mock.data.dotEnv + getToken.mockReturnValue('accessToken') + mockEventsSdkInstance.getAllRegistrationsForWorkspace.mockResolvedValue(mock.data.getAllWebhookRegistrationsWithEmptyResponse) + + mockEventsSdkInstance.createRegistration.mockReturnValue(mock.data.createWebhookRegistrationResponse) + await expect(hook({ appConfig: { project: mock.data.sampleProjectWithoutEvents, events: mock.data.sampleEvents }, force: true })).resolves.not.toThrowError() + expect(mockEventsSdkInstance.createRegistration).toBeCalledTimes(1) + expect(mockEventsSdkInstance.createRegistration).toHaveBeenCalledWith(CONSUMER_ID, PROJECT_ID, WORKSPACE_ID, + mock.data.hookDecodedEventRegistration1) + expect(mockEventsSdkInstance.deleteRegistration).toHaveBeenCalledTimes(0) + }) + + test('test error on delete registrations not part of the config', async () => { + const hook = require('../../src/hooks/post-deploy-event-reg') + expect(typeof hook).toBe('function') + process.env = mock.data.dotEnv + getToken.mockReturnValue('accessToken') + const events = { + registrations: { + 'Event Registration 1': mock.data.sampleEvents.registrations['Event Registration 1'] + } + } + mockEventsSdkInstance.getAllRegistrationsForWorkspace.mockResolvedValue(mock.data.getAllWebhookRegistrationsResponse) + mockEventsSdkInstance.updateRegistration.mockReturnValue(mock.data.createWebhookRegistrationResponse) + mockEventsSdkInstance.deleteRegistration.mockRejectedValueOnce({ + code: 500, + errorDetails: { + message: 'Internal Server Error' + } + }) + const projectWithEmptyEvents = mock.data.sampleProjectWithoutEvents + await expect(hook({ appConfig: { project: projectWithEmptyEvents, events }, force: true })).rejects.toThrowError() + expect(mockEventsSdkInstance.updateRegistration).toBeCalledTimes(1) + expect(mockEventsSdkInstance.deleteRegistration).toHaveBeenCalledWith(CONSUMER_ID, PROJECT_ID, WORKSPACE_ID, 'REGID2') + }) }) diff --git a/test/mocks.js b/test/mocks.js index 79e7aee..e73f13a 100644 --- a/test/mocks.js +++ b/test/mocks.js @@ -272,6 +272,12 @@ const createWebhookRegistrationResponse = { ...getWebhookRegistrationResponse } +const getAllWebhookRegistrationsWithEmptyResponse = { + _embedded: { + registrations: [] + } +} + const getAllWebhookRegistrationsResponse = { _embedded: { registrations: [ @@ -288,7 +294,7 @@ const getAllWebhookRegistrationsResponse = { } }, id: 11111, - name: 'bowling 1', + name: 'Event Registration 1', description: 'let me know when we can go play bowling!', client_id: '1234654902189324798', webhook_url: 'https://send-me-a-bowling-event.com/right-now', @@ -322,7 +328,7 @@ const getAllWebhookRegistrationsResponse = { } }, id: 22222, - name: 'table tenis 2', + name: 'Event Registration 2', description: 'registration for table tennis events', client_id: '1234654902189324798', webhook_url: 'https://send-me-a-table-tennis-event.com/please', @@ -597,6 +603,7 @@ const hookDecodedEventRegistration1 = { client_id: 'serviceApiKey', description: 'Registration for IO Events 1', delivery_type: 'webhook', + runtime_action: 'poc-event-1', events_of_interest: [{ provider_id: 'providerId1', event_code: 'eventCode1' @@ -660,6 +667,7 @@ const data = { getAllEventMetadata, createWebhookRegistrationResponse, getAllWebhookRegistrationsResponse, + getAllWebhookRegistrationsWithEmptyResponse, getWebhookRegistrationResponse, createWebhookRegistrationInputJSON, createWebhookRegistrationInputJSONNoClientId,