diff --git a/spec/main.spec.ts b/spec/main.spec.ts index ccc4193..d34d79c 100644 --- a/spec/main.spec.ts +++ b/spec/main.spec.ts @@ -24,15 +24,8 @@ import { expect } from 'chai'; import * as functions from 'firebase-functions'; import { set } from 'lodash'; -import { - mockConfig, - makeChange, - wrap, -} from '../src/main'; -import { - _makeResourceName, - _extractParams, -} from '../src/v1'; +import { mockConfig, makeChange, wrap } from '../src/main'; +import { _makeResourceName, _extractParams } from '../src/v1'; import { features } from '../src/features'; import { FirebaseFunctionsTest } from '../src/lifecycle'; import { alerts } from 'firebase-functions/v2'; diff --git a/spec/v2.spec.ts b/spec/v2.spec.ts index d605a4c..d2f133c 100644 --- a/spec/v2.spec.ts +++ b/spec/v2.spec.ts @@ -134,11 +134,12 @@ describe('v2', () => { describe('storage', () => { describe('storage.onObjectArchived()', () => { it('should update CloudEvent appropriately', () => { - const bucket = 'bucket'; + const bucket = 'bucket_override'; const cloudFn = storage.onObjectArchived(bucket, handler); const cloudFnWrap = wrapV2(cloudFn); expect(cloudFnWrap().cloudEvent).to.include({ bucket, + source: `//storage.googleapis.com/projects/_/buckets/${bucket}`, }); }); }); @@ -180,8 +181,21 @@ describe('v2', () => { const cloudFn = pubsub.onMessagePublished('topic', handler); const cloudFnWrap = wrapV2(cloudFn); - expect(cloudFnWrap().cloudEvent.data.message).to.include({ - data: 'eyJoZWxsbyI6IndvcmxkIn0=', // Note: Defined in the partial + const message = cloudFnWrap().cloudEvent.data.message; + const data = message.data; + const json = JSON.parse(Buffer.from(data, 'base64').toString('utf8')); + expect(message).deep.equal({ + // Mock data (can be overridden) + attributes: { + 'sample-attribute': 'I am an attribute', + }, + messageId: 'message_id', + + // Recapture publish time (since it's generated during runtime) + publishTime: message.publishTime, + + // Assertions on Expected Updates + data: Buffer.from(JSON.stringify(json)).toString('base64'), }); }); it('should update CloudEvent with json data override', () => { @@ -195,14 +209,24 @@ describe('v2', () => { const cloudFnWrap = wrapV2(cloudFn); const cloudEventPartial = { data }; - expect( - cloudFnWrap(cloudEventPartial).cloudEvent.data.message - ).to.include({ - data: 'eyJoZWxsbyI6IndvcmxkIn0=', // Note: This is a mismatch from the json + const message = cloudFnWrap(cloudEventPartial).cloudEvent.data + .message; + expect(message).deep.equal({ + // Mock data (can be overridden) + attributes: { + 'sample-attribute': 'I am an attribute', + }, + messageId: 'message_id', + + // Recapture publish time (since it's generated during runtime) + publishTime: message.publishTime, + + // Assertions on Expected Updates + data: Buffer.from(JSON.stringify(data.message.json)).toString( + 'base64' + ), + json: { firebase: 'test' }, }); - expect( - cloudFnWrap(cloudEventPartial).cloudEvent.data.message.json - ).to.include({ firebase: 'test' }); }); it('should update CloudEvent with json and data string overrides', () => { const data = { @@ -216,14 +240,24 @@ describe('v2', () => { const cloudFnWrap = wrapV2(cloudFn); const cloudEventPartial = { data }; - expect( - cloudFnWrap(cloudEventPartial).cloudEvent.data.message - ).to.include({ - data: 'eyJmaXJlYmFzZSI6Im5vbl9qc29uX3Rlc3QifQ==', + const message = cloudFnWrap(cloudEventPartial).cloudEvent.data + .message; + expect(message).deep.equal({ + // Mock data (can be overridden) + attributes: { + 'sample-attribute': 'I am an attribute', + }, + messageId: 'message_id', + + // Recapture publish time (since it's generated during runtime) + publishTime: message.publishTime, + + // Assertions on Expected Updates + data: Buffer.from(JSON.stringify(data.message.json)).toString( + 'base64' + ), + json: data.message.json, }); - expect( - cloudFnWrap(cloudEventPartial).cloudEvent.data.message.json - ).to.include({ firebase: 'non_json_test' }); }); }); }); diff --git a/src/cloudevent/generate.ts b/src/cloudevent/generate.ts index fffafa2..908648b 100644 --- a/src/cloudevent/generate.ts +++ b/src/cloudevent/generate.ts @@ -1,9 +1,8 @@ import { CloudEvent } from 'firebase-functions/v2'; import { CloudFunction } from 'firebase-functions/v2'; -import { LIST_OF_MOCK_CLOUD_EVENT_PARTIALS } from './partials/partials'; -import { DeepPartial, MockCloudEventPartials } from './types'; +import { LIST_OF_MOCK_CLOUD_EVENT_PARTIALS } from './mocks/partials'; +import { DeepPartial, MockCloudEventAbstractFactory } from './types'; import merge from 'ts-deepmerge'; -import { getEventType } from './partials/helpers'; /** * @return {CloudEvent} Generated Mock CloudEvent @@ -14,61 +13,27 @@ export function generateCombinedCloudEvent< cloudFunction: CloudFunction, cloudEventPartial?: DeepPartial ): EventType { - const generatedCloudEvent = generateMockCloudEvent(cloudFunction); + const generatedCloudEvent = generateMockCloudEvent( + cloudFunction, + cloudEventPartial + ); return cloudEventPartial ? (merge(generatedCloudEvent, cloudEventPartial) as EventType) : generatedCloudEvent; } export function generateMockCloudEvent>( - cloudFunction: CloudFunction -): EventType { - return { - ...generateBaseCloudEvent(cloudFunction), - ...generateMockCloudEventPartial(cloudFunction), - }; -} - -/** @internal */ -function generateBaseCloudEvent>( - cloudFunction: CloudFunction + cloudFunction: CloudFunction, + cloudEventPartial?: DeepPartial ): EventType { - // TODO: Consider refactoring so that we don't use this utility function. This - // is not type safe because EventType may require additional fields, which this - // function does not know how to satisfy. - // This could possibly be augmented to take a CloudEvent and AdditionalFields - // where AdditionalFields uses the keyof operator to make only new fields required. - return { - specversion: '1.0', - id: makeEventId(), - data: {}, - source: '', // Required field that will get overridden by Provider-specific MockCloudEventPartials - type: getEventType(cloudFunction), - time: new Date().toISOString(), - } as any; -} - -function generateMockCloudEventPartial>( - cloudFunction: CloudFunction -): DeepPartial { for (const mockCloudEventPartial of LIST_OF_MOCK_CLOUD_EVENT_PARTIALS) { if (mockCloudEventPartial.match(cloudFunction)) { - return (mockCloudEventPartial as MockCloudEventPartials< - EventType - >).generatePartial(cloudFunction); + return mockCloudEventPartial.generateMock( + cloudFunction, + cloudEventPartial + ); } } // No matches were found - return {}; -} - -function makeEventId(): string { - return ( - Math.random() - .toString(36) - .substring(2, 15) + - Math.random() - .toString(36) - .substring(2, 15) - ); + return null; } diff --git a/src/cloudevent/partials/alerts/alerts-on-alert-published.ts b/src/cloudevent/mocks/alerts/alerts-on-alert-published.ts similarity index 63% rename from src/cloudevent/partials/alerts/alerts-on-alert-published.ts rename to src/cloudevent/mocks/alerts/alerts-on-alert-published.ts index 1ca941d..a13c371 100644 --- a/src/cloudevent/partials/alerts/alerts-on-alert-published.ts +++ b/src/cloudevent/mocks/alerts/alerts-on-alert-published.ts @@ -1,20 +1,29 @@ -import { DeepPartial, MockCloudEventPartials } from '../../types'; +import { DeepPartial, MockCloudEventAbstractFactory } from '../../types'; import { CloudFunction } from 'firebase-functions/v2'; -import { APP_ID, getEventType, PROJECT_ID } from '../helpers'; +import { + APP_ID, + getBaseCloudEvent, + getEventType, + PROJECT_ID, +} from '../helpers'; import { FirebaseAlertData, AlertEvent } from 'firebase-functions/v2/alerts'; -export const alertsOnAlertPublished: MockCloudEventPartials> = { - generatePartial( - _: CloudFunction> - ): DeepPartial> { + generateMock( + cloudFunction: CloudFunction> + ): AlertEvent { const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`; const alertType = 'appDistribution.newTesterIosDevice'; const appId = APP_ID; return { + // Spread common fields + ...getBaseCloudEvent(cloudFunction), + // Spread fields specific to this CloudEvent + alertType, appId, data: getOnAlertPublishedData(), diff --git a/src/cloudevent/partials/alerts/app-distribution-on-new-tester-ios-device-published.ts b/src/cloudevent/mocks/alerts/app-distribution-on-new-tester-ios-device-published.ts similarity index 70% rename from src/cloudevent/partials/alerts/app-distribution-on-new-tester-ios-device-published.ts rename to src/cloudevent/mocks/alerts/app-distribution-on-new-tester-ios-device-published.ts index 4d8a22d..25581e1 100644 --- a/src/cloudevent/partials/alerts/app-distribution-on-new-tester-ios-device-published.ts +++ b/src/cloudevent/mocks/alerts/app-distribution-on-new-tester-ios-device-published.ts @@ -1,21 +1,29 @@ -import { DeepPartial, MockCloudEventPartials } from '../../types'; +import { DeepPartial, MockCloudEventAbstractFactory } from '../../types'; import { CloudFunction } from 'firebase-functions/v2'; -import { getEventFilters, getEventType, PROJECT_ID } from '../helpers'; +import { + getBaseCloudEvent, + getEventFilters, + getEventType, + PROJECT_ID, +} from '../helpers'; import { AppDistributionEvent, NewTesterDevicePayload, } from 'firebase-functions/v2/alerts/appDistribution'; -export const alertsAppDistributionOnNewTesterIosDevicePublished: MockCloudEventPartials> = { - generatePartial( - _: CloudFunction> - ): DeepPartial> { + generateMock( + cloudFunction: CloudFunction> + ): AppDistributionEvent { const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`; const now = new Date().toISOString(); return { + // Spread common fields + ...getBaseCloudEvent(cloudFunction), + // Spread fields specific to this CloudEvent source, data: { createTime: now, diff --git a/src/cloudevent/partials/alerts/billing-on-plan-automated-update-published.ts b/src/cloudevent/mocks/alerts/billing-on-plan-automated-update-published.ts similarity index 71% rename from src/cloudevent/partials/alerts/billing-on-plan-automated-update-published.ts rename to src/cloudevent/mocks/alerts/billing-on-plan-automated-update-published.ts index 2468094..67d7eee 100644 --- a/src/cloudevent/partials/alerts/billing-on-plan-automated-update-published.ts +++ b/src/cloudevent/mocks/alerts/billing-on-plan-automated-update-published.ts @@ -1,21 +1,29 @@ -import { DeepPartial, MockCloudEventPartials } from '../../types'; +import { DeepPartial, MockCloudEventAbstractFactory } from '../../types'; import { CloudFunction } from 'firebase-functions/v2'; import { FirebaseAlertData } from 'firebase-functions/v2/alerts'; import { BillingEvent, PlanAutomatedUpdatePayload, } from 'firebase-functions/v2/alerts/billing'; -import { getEventFilters, getEventType, PROJECT_ID } from '../helpers'; +import { + getBaseCloudEvent, + getEventFilters, + getEventType, + PROJECT_ID, +} from '../helpers'; -export const alertsBillingOnPlanAutomatedUpdatePublished: MockCloudEventPartials> = { - generatePartial( - _: CloudFunction> - ): DeepPartial> { + generateMock( + cloudFunction: CloudFunction> + ): BillingEvent { const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`; return { + // Spread common fields + ...getBaseCloudEvent(cloudFunction), + // Spread fields specific to this CloudEvent source, data: getBillingPlanAutomatedUpdateData(), }; diff --git a/src/cloudevent/partials/alerts/billing-on-plan-update-published.ts b/src/cloudevent/mocks/alerts/billing-on-plan-update-published.ts similarity index 68% rename from src/cloudevent/partials/alerts/billing-on-plan-update-published.ts rename to src/cloudevent/mocks/alerts/billing-on-plan-update-published.ts index b3d4f4d..90efd90 100644 --- a/src/cloudevent/partials/alerts/billing-on-plan-update-published.ts +++ b/src/cloudevent/mocks/alerts/billing-on-plan-update-published.ts @@ -1,21 +1,29 @@ -import { DeepPartial, MockCloudEventPartials } from '../../types'; +import { DeepPartial, MockCloudEventAbstractFactory } from '../../types'; import { CloudFunction } from 'firebase-functions/v2'; import { FirebaseAlertData } from 'firebase-functions/v2/alerts'; import { BillingEvent, PlanUpdatePayload, } from 'firebase-functions/v2/alerts/billing'; -import { getEventFilters, getEventType, PROJECT_ID } from '../helpers'; +import { + getBaseCloudEvent, + getEventFilters, + getEventType, + PROJECT_ID, +} from '../helpers'; -export const alertsBillingOnPlanUpdatePublished: MockCloudEventPartials> = { - generatePartial( - _: CloudFunction> - ): DeepPartial> { + generateMock( + cloudFunction: CloudFunction> + ): BillingEvent { const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`; return { + // Spread common fields + ...getBaseCloudEvent(cloudFunction), + // Spread fields specific to this CloudEvent source, data: getBillingPlanUpdateData(), }; diff --git a/src/cloudevent/partials/alerts/crashlytics-on-new-anr-issue-published.ts b/src/cloudevent/mocks/alerts/crashlytics-on-new-anr-issue-published.ts similarity index 73% rename from src/cloudevent/partials/alerts/crashlytics-on-new-anr-issue-published.ts rename to src/cloudevent/mocks/alerts/crashlytics-on-new-anr-issue-published.ts index b3764cb..1a465a6 100644 --- a/src/cloudevent/partials/alerts/crashlytics-on-new-anr-issue-published.ts +++ b/src/cloudevent/mocks/alerts/crashlytics-on-new-anr-issue-published.ts @@ -1,21 +1,29 @@ -import { DeepPartial, MockCloudEventPartials } from '../../types'; +import { DeepPartial, MockCloudEventAbstractFactory } from '../../types'; import { CloudFunction } from 'firebase-functions/v2'; -import { getEventFilters, getEventType, PROJECT_ID } from '../helpers'; +import { + getBaseCloudEvent, + getEventFilters, + getEventType, + PROJECT_ID, +} from '../helpers'; import { CrashlyticsEvent, NewAnrIssuePayload, } from 'firebase-functions/v2/alerts/crashlytics'; import { FirebaseAlertData } from 'firebase-functions/v2/alerts'; -export const alertsCrashlyticsOnNewAnrIssuePublished: MockCloudEventPartials> = { - generatePartial( - _: CloudFunction> - ): DeepPartial> { + generateMock( + cloudFunction: CloudFunction> + ): CrashlyticsEvent { const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`; return { + // Spread common fields + ...getBaseCloudEvent(cloudFunction), + // Spread fields specific to this CloudEvent source, data: getCrashlyticsNewAnrIssueData(), }; diff --git a/src/cloudevent/partials/alerts/crashlytics-on-new-fatal-issue-published.ts b/src/cloudevent/mocks/alerts/crashlytics-on-new-fatal-issue-published.ts similarity index 73% rename from src/cloudevent/partials/alerts/crashlytics-on-new-fatal-issue-published.ts rename to src/cloudevent/mocks/alerts/crashlytics-on-new-fatal-issue-published.ts index 34ae135..7cb8126 100644 --- a/src/cloudevent/partials/alerts/crashlytics-on-new-fatal-issue-published.ts +++ b/src/cloudevent/mocks/alerts/crashlytics-on-new-fatal-issue-published.ts @@ -1,21 +1,29 @@ -import { DeepPartial, MockCloudEventPartials } from '../../types'; +import { DeepPartial, MockCloudEventAbstractFactory } from '../../types'; import { CloudFunction } from 'firebase-functions/v2'; -import { getEventFilters, getEventType, PROJECT_ID } from '../helpers'; +import { + getBaseCloudEvent, + getEventFilters, + getEventType, + PROJECT_ID, +} from '../helpers'; import { CrashlyticsEvent, NewFatalIssuePayload, } from 'firebase-functions/v2/alerts/crashlytics'; import { FirebaseAlertData } from 'firebase-functions/v2/alerts'; -export const alertsCrashlyticsOnNewFatalIssuePublished: MockCloudEventPartials> = { - generatePartial( - _: CloudFunction> - ): DeepPartial> { + generateMock( + cloudFunction: CloudFunction> + ): CrashlyticsEvent { const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`; return { + // Spread common fields + ...getBaseCloudEvent(cloudFunction), + // Spread fields specific to this CloudEvent source, data: getCrashlyticsNewFatalIssueData(), }; diff --git a/src/cloudevent/partials/alerts/crashlytics-on-new-nonfatal-issue-published.ts b/src/cloudevent/mocks/alerts/crashlytics-on-new-nonfatal-issue-published.ts similarity index 73% rename from src/cloudevent/partials/alerts/crashlytics-on-new-nonfatal-issue-published.ts rename to src/cloudevent/mocks/alerts/crashlytics-on-new-nonfatal-issue-published.ts index 73b6c09..85a1c5c 100644 --- a/src/cloudevent/partials/alerts/crashlytics-on-new-nonfatal-issue-published.ts +++ b/src/cloudevent/mocks/alerts/crashlytics-on-new-nonfatal-issue-published.ts @@ -1,21 +1,29 @@ -import { DeepPartial, MockCloudEventPartials } from '../../types'; +import { DeepPartial, MockCloudEventAbstractFactory } from '../../types'; import { CloudFunction } from 'firebase-functions/v2'; -import { getEventFilters, getEventType, PROJECT_ID } from '../helpers'; +import { + getBaseCloudEvent, + getEventFilters, + getEventType, + PROJECT_ID, +} from '../helpers'; import { CrashlyticsEvent, NewNonfatalIssuePayload, } from 'firebase-functions/v2/alerts/crashlytics'; import { FirebaseAlertData } from 'firebase-functions/v2/alerts'; -export const alertsCrashlyticsOnNewNonfatalIssuePublished: MockCloudEventPartials> = { - generatePartial( - _: CloudFunction> - ): DeepPartial> { + generateMock( + cloudFunction: CloudFunction> + ): CrashlyticsEvent { const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`; return { + // Spread common fields + ...getBaseCloudEvent(cloudFunction), + // Spread fields specific to this CloudEvent source, data: getCrashlyticsNewNonfatalIssueData(), }; diff --git a/src/cloudevent/partials/alerts/crashlytics-on-regression-alert-published.ts b/src/cloudevent/mocks/alerts/crashlytics-on-regression-alert-published.ts similarity index 74% rename from src/cloudevent/partials/alerts/crashlytics-on-regression-alert-published.ts rename to src/cloudevent/mocks/alerts/crashlytics-on-regression-alert-published.ts index 1b1fe23..a23c351 100644 --- a/src/cloudevent/partials/alerts/crashlytics-on-regression-alert-published.ts +++ b/src/cloudevent/mocks/alerts/crashlytics-on-regression-alert-published.ts @@ -1,21 +1,29 @@ -import { DeepPartial, MockCloudEventPartials } from '../../types'; +import { DeepPartial, MockCloudEventAbstractFactory } from '../../types'; import { CloudFunction } from 'firebase-functions/v2'; -import { getEventFilters, getEventType, PROJECT_ID } from '../helpers'; +import { + getBaseCloudEvent, + getEventFilters, + getEventType, + PROJECT_ID, +} from '../helpers'; import { CrashlyticsEvent, RegressionAlertPayload, } from 'firebase-functions/v2/alerts/crashlytics'; import { FirebaseAlertData } from 'firebase-functions/v2/alerts'; -export const alertsCrashlyticsOnRegressionAlertPublished: MockCloudEventPartials> = { - generatePartial( - _: CloudFunction> - ): DeepPartial> { + generateMock( + cloudFunction: CloudFunction> + ): CrashlyticsEvent { const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`; return { + // Spread common fields + ...getBaseCloudEvent(cloudFunction), + // Spread fields specific to this CloudEvent source, data: getCrashlyticsRegressionAlertPayload(), }; diff --git a/src/cloudevent/partials/alerts/crashlytics-on-stability-digest-published.ts b/src/cloudevent/mocks/alerts/crashlytics-on-stability-digest-published.ts similarity index 76% rename from src/cloudevent/partials/alerts/crashlytics-on-stability-digest-published.ts rename to src/cloudevent/mocks/alerts/crashlytics-on-stability-digest-published.ts index 7459ac0..3b22992 100644 --- a/src/cloudevent/partials/alerts/crashlytics-on-stability-digest-published.ts +++ b/src/cloudevent/mocks/alerts/crashlytics-on-stability-digest-published.ts @@ -1,21 +1,29 @@ -import { DeepPartial, MockCloudEventPartials } from '../../types'; +import { DeepPartial, MockCloudEventAbstractFactory } from '../../types'; import { CloudFunction } from 'firebase-functions/v2'; -import { getEventFilters, getEventType, PROJECT_ID } from '../helpers'; +import { + getBaseCloudEvent, + getEventFilters, + getEventType, + PROJECT_ID, +} from '../helpers'; import { CrashlyticsEvent, StabilityDigestPayload, } from 'firebase-functions/v2/alerts/crashlytics'; import { FirebaseAlertData } from 'firebase-functions/v2/alerts'; -export const alertsCrashlyticsOnStabilityDigestPublished: MockCloudEventPartials> = { - generatePartial( - _: CloudFunction> - ): DeepPartial> { + generateMock( + cloudFunction: CloudFunction> + ): CrashlyticsEvent { const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`; return { + // Spread common fields + ...getBaseCloudEvent(cloudFunction), + // Spread fields specific to this CloudEvent source, data: getCrashlyticsStabilityData(), }; diff --git a/src/cloudevent/partials/alerts/crashlytics-on-velocity-alert-published.ts b/src/cloudevent/mocks/alerts/crashlytics-on-velocity-alert-published.ts similarity index 75% rename from src/cloudevent/partials/alerts/crashlytics-on-velocity-alert-published.ts rename to src/cloudevent/mocks/alerts/crashlytics-on-velocity-alert-published.ts index af96e13..386e9cc 100644 --- a/src/cloudevent/partials/alerts/crashlytics-on-velocity-alert-published.ts +++ b/src/cloudevent/mocks/alerts/crashlytics-on-velocity-alert-published.ts @@ -1,21 +1,29 @@ -import { DeepPartial, MockCloudEventPartials } from '../../types'; +import { DeepPartial, MockCloudEventAbstractFactory } from '../../types'; import { CloudFunction } from 'firebase-functions/v2'; -import { getEventFilters, getEventType, PROJECT_ID } from '../helpers'; +import { + getBaseCloudEvent, + getEventFilters, + getEventType, + PROJECT_ID, +} from '../helpers'; import { CrashlyticsEvent, VelocityAlertPayload, } from 'firebase-functions/v2/alerts/crashlytics'; import { FirebaseAlertData } from 'firebase-functions/v2/alerts'; -export const alertsCrashlyticsOnVelocityAlertPublished: MockCloudEventPartials> = { - generatePartial( - _: CloudFunction> - ): DeepPartial> { + generateMock( + cloudFunction: CloudFunction> + ): CrashlyticsEvent { const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`; return { + // Spread common fields + ...getBaseCloudEvent(cloudFunction), + // Spread fields specific to this CloudEvent source, data: getCrashlyticsVelocityAlertData(), }; diff --git a/src/cloudevent/mocks/eventarc/eventarc-on-custom-event-published.ts b/src/cloudevent/mocks/eventarc/eventarc-on-custom-event-published.ts new file mode 100644 index 0000000..7043875 --- /dev/null +++ b/src/cloudevent/mocks/eventarc/eventarc-on-custom-event-published.ts @@ -0,0 +1,25 @@ +import { DeepPartial, MockCloudEventAbstractFactory } from '../../types'; +import { CloudEvent, CloudFunction } from 'firebase-functions/v2'; +import { getBaseCloudEvent } from '../helpers'; + +export const eventarcOnCustomEventPublished: MockCloudEventAbstractFactory = { + generateMock( + cloudFunction: CloudFunction>, + cloudEventPartial?: DeepPartial> + ): CloudEvent { + const source = 'eventarc_source'; + const subject = 'eventarc_subject'; + + return { + // Spread common fields + ...getBaseCloudEvent(cloudFunction), + // Spread fields specific to this CloudEvent + data: cloudEventPartial?.data || {}, + source, + subject, + }; + }, + match(_: CloudFunction>): boolean { + return true; + }, +}; diff --git a/src/cloudevent/mocks/helpers.ts b/src/cloudevent/mocks/helpers.ts new file mode 100644 index 0000000..e0f76e4 --- /dev/null +++ b/src/cloudevent/mocks/helpers.ts @@ -0,0 +1,40 @@ +import { CloudEvent, CloudFunction } from 'firebase-functions/v2'; +import { DeepPartial } from '../types'; + +export const APP_ID = '__APP_ID__'; +export const PROJECT_ID = '42'; +export const FILENAME = 'file_name'; + +export function getEventType(cloudFunction: CloudFunction): string { + return cloudFunction?.__endpoint?.eventTrigger?.eventType || ''; +} + +export function getEventFilters( + cloudFunction: CloudFunction +): Record { + return cloudFunction?.__endpoint?.eventTrigger?.eventFilters || {}; +} + +export function getBaseCloudEvent>( + cloudFunction: CloudFunction +): EventType { + return { + specversion: '1.0', + id: makeEventId(), + data: undefined, + source: '', // Required field that will get overridden by Provider-specific MockCloudEventPartials + type: getEventType(cloudFunction), + time: new Date().toISOString(), + } as EventType; +} + +function makeEventId(): string { + return ( + Math.random() + .toString(36) + .substring(2, 15) + + Math.random() + .toString(36) + .substring(2, 15) + ); +} diff --git a/src/cloudevent/partials/partials.ts b/src/cloudevent/mocks/partials.ts similarity index 96% rename from src/cloudevent/partials/partials.ts rename to src/cloudevent/mocks/partials.ts index 0f5e6ce..116c9d5 100644 --- a/src/cloudevent/partials/partials.ts +++ b/src/cloudevent/mocks/partials.ts @@ -1,4 +1,4 @@ -import { MockCloudEventPartials } from '../types'; +import { MockCloudEventAbstractFactory } from '../types'; import { alertsOnAlertPublished } from './alerts/alerts-on-alert-published'; import { alertsCrashlyticsOnNewAnrIssuePublished } from './alerts/crashlytics-on-new-anr-issue-published'; import { alertsCrashlyticsOnNewFatalIssuePublished } from './alerts/crashlytics-on-new-fatal-issue-published'; @@ -18,7 +18,7 @@ import { storageV1 } from './storage'; * (eg {@link alertsOnAlertPublished}). In addition, * {@link eventarcOnCustomEventPublished} acts as a catch-all. */ -export const LIST_OF_MOCK_CLOUD_EVENT_PARTIALS: Array> = [ alertsCrashlyticsOnNewAnrIssuePublished, diff --git a/src/cloudevent/mocks/pubsub/pubsub-on-message-published.ts b/src/cloudevent/mocks/pubsub/pubsub-on-message-published.ts new file mode 100644 index 0000000..78a0af7 --- /dev/null +++ b/src/cloudevent/mocks/pubsub/pubsub-on-message-published.ts @@ -0,0 +1,67 @@ +import { DeepPartial, MockCloudEventAbstractFactory } from '../../types'; +import { CloudEvent, CloudFunction, pubsub } from 'firebase-functions/v2'; +import { + getBaseCloudEvent, + getEventFilters, + getEventType, + PROJECT_ID, +} from '../helpers'; + +export const pubsubOnMessagePublished: MockCloudEventAbstractFactory> = { + generateMock( + cloudFunction: CloudFunction>, + cloudEventPartial?: DeepPartial> + ): CloudEvent { + const topicId = getEventFilters(cloudFunction)?.topic || ''; + const source = `//pubsub.googleapis.com/projects/${PROJECT_ID}/topics/${topicId}`; + const subscription = `projects/${PROJECT_ID}/subscriptions/pubsubexample-1`; + + // Used if no data.message.json is provided by the partial; + const dataMessageJsonDefault = { hello: 'world' }; + const dataMessageAttributesDefault = { + 'sample-attribute': 'I am an attribute', + }; + + const dataMessageJson = + cloudEventPartial?.data?.message?.json || dataMessageJsonDefault; + + // We should respect if the user provides their own message.data. + const dataMessageData = + cloudEventPartial?.data?.message?.data || + Buffer.from(JSON.stringify(dataMessageJson)).toString('base64'); + + // TODO - consider warning the user if their data does not match the json they provide + + const messageData = { + data: dataMessageData, + messageId: cloudEventPartial?.data?.message?.messageId || 'message_id', + attributes: + cloudEventPartial?.data?.message?.attributes || + dataMessageAttributesDefault, + }; + const message = new pubsub.Message(messageData); + + return { + // Spread common fields + ...getBaseCloudEvent(cloudFunction), + // Spread fields specific to this CloudEvent + source, + data: { + /** + * Note: Its very important we return the JSON representation of the message here. Without it, + * ts-merge blows away `data.message`. + */ + message: message.toJSON(), + subscription, + }, + }; + }, + match(cloudFunction: CloudFunction>): boolean { + return ( + getEventType(cloudFunction) === + 'google.cloud.pubsub.topic.v1.messagePublished' + ); + }, +}; diff --git a/src/cloudevent/mocks/storage/index.ts b/src/cloudevent/mocks/storage/index.ts new file mode 100644 index 0000000..bf2e221 --- /dev/null +++ b/src/cloudevent/mocks/storage/index.ts @@ -0,0 +1,42 @@ +import { DeepPartial, MockCloudEventAbstractFactory } from '../../types'; +import { CloudFunction, CloudEvent } from 'firebase-functions/v2'; +import { StorageEvent } from 'firebase-functions/v2/storage'; +import { + FILENAME, + getBaseCloudEvent, + getEventFilters, + getEventType, +} from '../helpers'; +import { getStorageObjectData } from './storage-data'; +import { pubsub } from 'firebase-functions/lib/v2'; + +export const storageV1: MockCloudEventAbstractFactory = { + generateMock( + cloudFunction: CloudFunction, + cloudEventPartial?: DeepPartial + ): StorageEvent { + const bucket = + cloudEventPartial?.bucket || + getEventFilters(cloudFunction)?.bucket || + 'bucket_name'; + const source = + cloudEventPartial?.source || + `//storage.googleapis.com/projects/_/buckets/${bucket}`; + const subject = cloudEventPartial?.subject || `objects/${FILENAME}`; + + return { + // Spread common fields + ...getBaseCloudEvent(cloudFunction), + // Spread fields specific to this CloudEvent + bucket, + source, + subject, + data: getStorageObjectData(bucket, FILENAME, 1), + }; + }, + match(cloudFunction: CloudFunction>): boolean { + return getEventType(cloudFunction).startsWith( + 'google.cloud.storage.object.v1' + ); + }, +}; diff --git a/src/cloudevent/partials/storage/storage-data.ts b/src/cloudevent/mocks/storage/storage-data.ts similarity index 100% rename from src/cloudevent/partials/storage/storage-data.ts rename to src/cloudevent/mocks/storage/storage-data.ts diff --git a/src/cloudevent/partials/eventarc/eventarc-on-custom-event-published.ts b/src/cloudevent/partials/eventarc/eventarc-on-custom-event-published.ts deleted file mode 100644 index 7c55bb3..0000000 --- a/src/cloudevent/partials/eventarc/eventarc-on-custom-event-published.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { DeepPartial, MockCloudEventPartials } from '../../types'; -import { CloudEvent, CloudFunction } from 'firebase-functions/v2'; - -export const eventarcOnCustomEventPublished: MockCloudEventPartials = { - generatePartial( - _: CloudFunction> - ): DeepPartial> { - const source = 'eventarc_source'; - const subject = 'eventarc_subject'; - - return { - data: {}, - source, - subject, - }; - }, - match(_: CloudFunction>): boolean { - return true; - }, -}; diff --git a/src/cloudevent/partials/helpers.ts b/src/cloudevent/partials/helpers.ts deleted file mode 100644 index 19f61fb..0000000 --- a/src/cloudevent/partials/helpers.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CloudEvent, CloudFunction } from 'firebase-functions/v2'; - -export const APP_ID = '__APP_ID__'; -export const PROJECT_ID = '42'; -export const FILENAME = 'file_name'; - -export function getEventType(cloudFunction: CloudFunction): string { - return cloudFunction?.__endpoint?.eventTrigger?.eventType || ''; -} - -export function getEventFilters( - cloudFunction: CloudFunction -): Record { - return cloudFunction?.__endpoint?.eventTrigger?.eventFilters || {}; -} diff --git a/src/cloudevent/partials/pubsub/pubsub-on-message-published.ts b/src/cloudevent/partials/pubsub/pubsub-on-message-published.ts deleted file mode 100644 index cbb0bbe..0000000 --- a/src/cloudevent/partials/pubsub/pubsub-on-message-published.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { DeepPartial, MockCloudEventPartials } from '../../types'; -import { CloudEvent, CloudFunction, pubsub } from 'firebase-functions/v2'; -import { getEventFilters, getEventType, PROJECT_ID } from '../helpers'; - -export const pubsubOnMessagePublished: MockCloudEventPartials> = { - generatePartial( - cloudFunction: CloudFunction> - ): DeepPartial> { - const topicId = getEventFilters(cloudFunction)?.topic || ''; - const source = `//pubsub.googleapis.com/projects/${PROJECT_ID}/topics/${topicId}`; - const subscription = `projects/${PROJECT_ID}/subscriptions/pubsubexample-1`; - - /** - * Node: The following is a Base64 string generated by the following: - * Buffer.from(dataMessageJson).toString('base64') - */ - const dataMessageData = 'eyJoZWxsbyI6IndvcmxkIn0='; - - return { - source, - data: { - message: { - attributes: { 'sample-attribute': 'I am an attribute' }, - data: dataMessageData, // Buffer base64 encoded - messageId: 'messageId', - publishTime: new Date().toISOString(), - }, - subscription, - }, - }; - }, - match(cloudFunction: CloudFunction>): boolean { - return ( - getEventType(cloudFunction) === - 'google.cloud.pubsub.topic.v1.messagePublished' - ); - }, -}; diff --git a/src/cloudevent/partials/storage/index.ts b/src/cloudevent/partials/storage/index.ts deleted file mode 100644 index 301d6ff..0000000 --- a/src/cloudevent/partials/storage/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DeepPartial, MockCloudEventPartials } from '../../types'; -import { CloudFunction, CloudEvent } from 'firebase-functions/v2'; -import { StorageEvent } from 'firebase-functions/v2/storage'; -import { FILENAME, getEventFilters, getEventType } from '../helpers'; -import { getStorageObjectData } from './storage-data'; - -export const storageV1: MockCloudEventPartials = { - generatePartial( - cloudFunction: CloudFunction - ): DeepPartial { - const bucket = getEventFilters(cloudFunction)?.bucket || 'bucket_name'; - const source = `//storage.googleapis.com/projects/_/buckets/${bucket}`; - const subject = `objects/${FILENAME}`; - - return { - bucket, - source, - subject, - data: getStorageObjectData(bucket, FILENAME, 1), - }; - }, - match(cloudFunction: CloudFunction>): boolean { - return getEventType(cloudFunction).startsWith( - 'google.cloud.storage.object.v1' - ); - }, -}; diff --git a/src/cloudevent/types.ts b/src/cloudevent/types.ts index 4ae593c..ab51ad5 100644 --- a/src/cloudevent/types.ts +++ b/src/cloudevent/types.ts @@ -3,14 +3,17 @@ import { CloudEvent, CloudFunction } from 'firebase-functions/v2'; export type DeepPartial = { [Key in keyof T]?: T[Key] extends object ? DeepPartial : T[Key]; }; -type MockCloudEventPartialFunction> = ( - cloudFunction: CloudFunction -) => DeepPartial; +type MockCloudEventFunction> = ( + cloudFunction: CloudFunction, + cloudEventPartial?: DeepPartial +) => EventType; type MockCloudEventMatchFunction> = ( cloudFunction: CloudFunction ) => boolean; -export interface MockCloudEventPartials> { - generatePartial: MockCloudEventPartialFunction; +export interface MockCloudEventAbstractFactory< + EventType extends CloudEvent +> { + generateMock: MockCloudEventFunction; match: MockCloudEventMatchFunction; }