Skip to content

Refactor MockCloudEvent generation to include partial #146

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 18, 2022
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
11 changes: 2 additions & 9 deletions spec/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
68 changes: 51 additions & 17 deletions spec/v2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
});
});
});
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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 = {
Expand All @@ -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' });
});
});
});
Expand Down
61 changes: 13 additions & 48 deletions src/cloudevent/generate.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,61 +13,27 @@ export function generateCombinedCloudEvent<
cloudFunction: CloudFunction<EventType>,
cloudEventPartial?: DeepPartial<EventType>
): EventType {
const generatedCloudEvent = generateMockCloudEvent(cloudFunction);
const generatedCloudEvent = generateMockCloudEvent(
cloudFunction,
cloudEventPartial
);
return cloudEventPartial
? (merge(generatedCloudEvent, cloudEventPartial) as EventType)
: generatedCloudEvent;
}

export function generateMockCloudEvent<EventType extends CloudEvent<unknown>>(
cloudFunction: CloudFunction<EventType>
): EventType {
return {
...generateBaseCloudEvent(cloudFunction),
...generateMockCloudEventPartial(cloudFunction),
};
}

/** @internal */
function generateBaseCloudEvent<EventType extends CloudEvent<unknown>>(
cloudFunction: CloudFunction<EventType>
cloudFunction: CloudFunction<EventType>,
cloudEventPartial?: DeepPartial<EventType>
): 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<unknown> and AdditionalFields<EventType>
// 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<EventType extends CloudEvent<unknown>>(
cloudFunction: CloudFunction<EventType>
): DeepPartial<EventType> {
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;
}
Original file line number Diff line number Diff line change
@@ -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<AlertEvent<
export const alertsOnAlertPublished: MockCloudEventAbstractFactory<AlertEvent<
FirebaseAlertData
>> = {
generatePartial(
_: CloudFunction<AlertEvent<FirebaseAlertData>>
): DeepPartial<AlertEvent<FirebaseAlertData>> {
generateMock(
cloudFunction: CloudFunction<AlertEvent<FirebaseAlertData>>
): AlertEvent<FirebaseAlertData> {
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(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AppDistributionEvent<
export const alertsAppDistributionOnNewTesterIosDevicePublished: MockCloudEventAbstractFactory<AppDistributionEvent<
NewTesterDevicePayload
>> = {
generatePartial(
_: CloudFunction<AppDistributionEvent<NewTesterDevicePayload>>
): DeepPartial<AppDistributionEvent<NewTesterDevicePayload>> {
generateMock(
cloudFunction: CloudFunction<AppDistributionEvent<NewTesterDevicePayload>>
): AppDistributionEvent<NewTesterDevicePayload> {
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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BillingEvent<
export const alertsBillingOnPlanAutomatedUpdatePublished: MockCloudEventAbstractFactory<BillingEvent<
PlanAutomatedUpdatePayload
>> = {
generatePartial(
_: CloudFunction<BillingEvent<PlanAutomatedUpdatePayload>>
): DeepPartial<BillingEvent<PlanAutomatedUpdatePayload>> {
generateMock(
cloudFunction: CloudFunction<BillingEvent<PlanAutomatedUpdatePayload>>
): BillingEvent<PlanAutomatedUpdatePayload> {
const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;

return {
// Spread common fields
...getBaseCloudEvent(cloudFunction),
// Spread fields specific to this CloudEvent
source,
data: getBillingPlanAutomatedUpdateData(),
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BillingEvent<
export const alertsBillingOnPlanUpdatePublished: MockCloudEventAbstractFactory<BillingEvent<
PlanUpdatePayload
>> = {
generatePartial(
_: CloudFunction<BillingEvent<PlanUpdatePayload>>
): DeepPartial<BillingEvent<PlanUpdatePayload>> {
generateMock(
cloudFunction: CloudFunction<BillingEvent<PlanUpdatePayload>>
): BillingEvent<PlanUpdatePayload> {
const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;

return {
// Spread common fields
...getBaseCloudEvent(cloudFunction),
// Spread fields specific to this CloudEvent
source,
data: getBillingPlanUpdateData(),
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CrashlyticsEvent<
export const alertsCrashlyticsOnNewAnrIssuePublished: MockCloudEventAbstractFactory<CrashlyticsEvent<
NewAnrIssuePayload
>> = {
generatePartial(
_: CloudFunction<CrashlyticsEvent<NewAnrIssuePayload>>
): DeepPartial<CrashlyticsEvent<NewAnrIssuePayload>> {
generateMock(
cloudFunction: CloudFunction<CrashlyticsEvent<NewAnrIssuePayload>>
): CrashlyticsEvent<NewAnrIssuePayload> {
const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;

return {
// Spread common fields
...getBaseCloudEvent(cloudFunction),
// Spread fields specific to this CloudEvent
source,
data: getCrashlyticsNewAnrIssueData(),
};
Expand Down
Loading