Skip to content

Commit 05054d2

Browse files
authored
Refactor MockCloudEvent generation to include partial (#146)
* Refactor MockCloudEvent generation to include user's Partial<CloudEvent> The goal of this commit is to incorporate the user's partial into the generated CloudEvent. All the AbstractFactories for Partial Cloud Events now return Cloud Events, and they all may incorporate the user provided partial when creating the Mock Cloud Event. Theres a few key benefits to this: 1. Type safety. The Partial Factories were not providing all the necessary fields required (as is the nature of the Partial..) 1. Consistency with other fields. Eg: If a user updates the bucket for storage event, lets attempt to update the other fields too. 1. Opaque fields - If we want the user provided partial to take precedence, we can easily control that. (see PubSub and EventArc factories) 1. Easier to handle one-off behavior. Not all the events behave the same way, particularly with PubSub. Although this is a "larger" change, the end-user contract remains intact. This does fix several bugs. Output is different for PubSub, EventArc, and Storage.
1 parent 702a602 commit 05054d2

23 files changed

+389
-234
lines changed

spec/v2.spec.ts

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,12 @@ describe('v2', () => {
134134
describe('storage', () => {
135135
describe('storage.onObjectArchived()', () => {
136136
it('should update CloudEvent appropriately', () => {
137-
const bucket = 'bucket';
137+
const bucket = 'bucket_override';
138138
const cloudFn = storage.onObjectArchived(bucket, handler);
139139
const cloudFnWrap = wrapV2(cloudFn);
140140
expect(cloudFnWrap().cloudEvent).to.include({
141141
bucket,
142+
source: `//storage.googleapis.com/projects/_/buckets/${bucket}`,
142143
});
143144
});
144145
});
@@ -180,8 +181,21 @@ describe('v2', () => {
180181
const cloudFn = pubsub.onMessagePublished('topic', handler);
181182
const cloudFnWrap = wrapV2(cloudFn);
182183

183-
expect(cloudFnWrap().cloudEvent.data.message).to.include({
184-
data: 'eyJoZWxsbyI6IndvcmxkIn0=', // Note: Defined in the partial
184+
const message = cloudFnWrap().cloudEvent.data.message;
185+
const data = message.data;
186+
const json = JSON.parse(Buffer.from(data, 'base64').toString('utf8'));
187+
expect(message).deep.equal({
188+
// Mock data (can be overridden)
189+
attributes: {
190+
'sample-attribute': 'I am an attribute',
191+
},
192+
messageId: 'message_id',
193+
194+
// Recapture publish time (since it's generated during runtime)
195+
publishTime: message.publishTime,
196+
197+
// Assertions on Expected Updates
198+
data: Buffer.from(JSON.stringify(json)).toString('base64'),
185199
});
186200
});
187201
it('should update CloudEvent with json data override', () => {
@@ -195,14 +209,24 @@ describe('v2', () => {
195209
const cloudFnWrap = wrapV2(cloudFn);
196210
const cloudEventPartial = { data };
197211

198-
expect(
199-
cloudFnWrap(cloudEventPartial).cloudEvent.data.message
200-
).to.include({
201-
data: 'eyJoZWxsbyI6IndvcmxkIn0=', // Note: This is a mismatch from the json
212+
const message = cloudFnWrap(cloudEventPartial).cloudEvent.data
213+
.message;
214+
expect(message).deep.equal({
215+
// Mock data (can be overridden)
216+
attributes: {
217+
'sample-attribute': 'I am an attribute',
218+
},
219+
messageId: 'message_id',
220+
221+
// Recapture publish time (since it's generated during runtime)
222+
publishTime: message.publishTime,
223+
224+
// Assertions on Expected Updates
225+
data: Buffer.from(JSON.stringify(data.message.json)).toString(
226+
'base64'
227+
),
228+
json: { firebase: 'test' },
202229
});
203-
expect(
204-
cloudFnWrap(cloudEventPartial).cloudEvent.data.message.json
205-
).to.include({ firebase: 'test' });
206230
});
207231
it('should update CloudEvent with json and data string overrides', () => {
208232
const data = {
@@ -216,14 +240,24 @@ describe('v2', () => {
216240
const cloudFnWrap = wrapV2(cloudFn);
217241
const cloudEventPartial = { data };
218242

219-
expect(
220-
cloudFnWrap(cloudEventPartial).cloudEvent.data.message
221-
).to.include({
222-
data: 'eyJmaXJlYmFzZSI6Im5vbl9qc29uX3Rlc3QifQ==',
243+
const message = cloudFnWrap(cloudEventPartial).cloudEvent.data
244+
.message;
245+
expect(message).deep.equal({
246+
// Mock data (can be overridden)
247+
attributes: {
248+
'sample-attribute': 'I am an attribute',
249+
},
250+
messageId: 'message_id',
251+
252+
// Recapture publish time (since it's generated during runtime)
253+
publishTime: message.publishTime,
254+
255+
// Assertions on Expected Updates
256+
data: Buffer.from(JSON.stringify(data.message.json)).toString(
257+
'base64'
258+
),
259+
json: data.message.json,
223260
});
224-
expect(
225-
cloudFnWrap(cloudEventPartial).cloudEvent.data.message.json
226-
).to.include({ firebase: 'non_json_test' });
227261
});
228262
});
229263
});

src/cloudevent/generate.ts

Lines changed: 13 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { CloudEvent } from 'firebase-functions/v2';
22
import { CloudFunction } from 'firebase-functions/v2';
3-
import { LIST_OF_MOCK_CLOUD_EVENT_PARTIALS } from './partials/partials';
4-
import { DeepPartial, MockCloudEventPartials } from './types';
3+
import { LIST_OF_MOCK_CLOUD_EVENT_PARTIALS } from './mocks/partials';
4+
import { DeepPartial, MockCloudEventAbstractFactory } from './types';
55
import merge from 'ts-deepmerge';
6-
import { getEventType } from './partials/helpers';
76

87
/**
98
* @return {CloudEvent} Generated Mock CloudEvent
@@ -14,61 +13,27 @@ export function generateCombinedCloudEvent<
1413
cloudFunction: CloudFunction<EventType>,
1514
cloudEventPartial?: DeepPartial<EventType>
1615
): EventType {
17-
const generatedCloudEvent = generateMockCloudEvent(cloudFunction);
16+
const generatedCloudEvent = generateMockCloudEvent(
17+
cloudFunction,
18+
cloudEventPartial
19+
);
1820
return cloudEventPartial
1921
? (merge(generatedCloudEvent, cloudEventPartial) as EventType)
2022
: generatedCloudEvent;
2123
}
2224

2325
export function generateMockCloudEvent<EventType extends CloudEvent<unknown>>(
24-
cloudFunction: CloudFunction<EventType>
25-
): EventType {
26-
return {
27-
...generateBaseCloudEvent(cloudFunction),
28-
...generateMockCloudEventPartial(cloudFunction),
29-
};
30-
}
31-
32-
/** @internal */
33-
function generateBaseCloudEvent<EventType extends CloudEvent<unknown>>(
34-
cloudFunction: CloudFunction<EventType>
26+
cloudFunction: CloudFunction<EventType>,
27+
cloudEventPartial?: DeepPartial<EventType>
3528
): EventType {
36-
// TODO: Consider refactoring so that we don't use this utility function. This
37-
// is not type safe because EventType may require additional fields, which this
38-
// function does not know how to satisfy.
39-
// This could possibly be augmented to take a CloudEvent<unknown> and AdditionalFields<EventType>
40-
// where AdditionalFields uses the keyof operator to make only new fields required.
41-
return {
42-
specversion: '1.0',
43-
id: makeEventId(),
44-
data: {},
45-
source: '', // Required field that will get overridden by Provider-specific MockCloudEventPartials
46-
type: getEventType(cloudFunction),
47-
time: new Date().toISOString(),
48-
} as any;
49-
}
50-
51-
function generateMockCloudEventPartial<EventType extends CloudEvent<unknown>>(
52-
cloudFunction: CloudFunction<EventType>
53-
): DeepPartial<EventType> {
5429
for (const mockCloudEventPartial of LIST_OF_MOCK_CLOUD_EVENT_PARTIALS) {
5530
if (mockCloudEventPartial.match(cloudFunction)) {
56-
return (mockCloudEventPartial as MockCloudEventPartials<
57-
EventType
58-
>).generatePartial(cloudFunction);
31+
return mockCloudEventPartial.generateMock(
32+
cloudFunction,
33+
cloudEventPartial
34+
);
5935
}
6036
}
6137
// No matches were found
62-
return {};
63-
}
64-
65-
function makeEventId(): string {
66-
return (
67-
Math.random()
68-
.toString(36)
69-
.substring(2, 15) +
70-
Math.random()
71-
.toString(36)
72-
.substring(2, 15)
73-
);
38+
return null;
7439
}

src/cloudevent/partials/alerts/alerts-on-alert-published.ts renamed to src/cloudevent/mocks/alerts/alerts-on-alert-published.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
1-
import { DeepPartial, MockCloudEventPartials } from '../../types';
1+
import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
22
import { CloudFunction } from 'firebase-functions/v2';
3-
import { APP_ID, getEventType, PROJECT_ID } from '../helpers';
3+
import {
4+
APP_ID,
5+
getBaseCloudEvent,
6+
getEventType,
7+
PROJECT_ID,
8+
} from '../helpers';
49
import { FirebaseAlertData, AlertEvent } from 'firebase-functions/v2/alerts';
510

6-
export const alertsOnAlertPublished: MockCloudEventPartials<AlertEvent<
11+
export const alertsOnAlertPublished: MockCloudEventAbstractFactory<AlertEvent<
712
FirebaseAlertData
813
>> = {
9-
generatePartial(
10-
_: CloudFunction<AlertEvent<FirebaseAlertData>>
11-
): DeepPartial<AlertEvent<FirebaseAlertData>> {
14+
generateMock(
15+
cloudFunction: CloudFunction<AlertEvent<FirebaseAlertData>>
16+
): AlertEvent<FirebaseAlertData> {
1217
const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
1318

1419
const alertType = 'appDistribution.newTesterIosDevice';
1520
const appId = APP_ID;
1621

1722
return {
23+
// Spread common fields
24+
...getBaseCloudEvent(cloudFunction),
25+
// Spread fields specific to this CloudEvent
26+
1827
alertType,
1928
appId,
2029
data: getOnAlertPublishedData(),

src/cloudevent/partials/alerts/app-distribution-on-new-tester-ios-device-published.ts renamed to src/cloudevent/mocks/alerts/app-distribution-on-new-tester-ios-device-published.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
1-
import { DeepPartial, MockCloudEventPartials } from '../../types';
1+
import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
22
import { CloudFunction } from 'firebase-functions/v2';
3-
import { getEventFilters, getEventType, PROJECT_ID } from '../helpers';
3+
import {
4+
getBaseCloudEvent,
5+
getEventFilters,
6+
getEventType,
7+
PROJECT_ID,
8+
} from '../helpers';
49
import {
510
AppDistributionEvent,
611
NewTesterDevicePayload,
712
} from 'firebase-functions/v2/alerts/appDistribution';
813

9-
export const alertsAppDistributionOnNewTesterIosDevicePublished: MockCloudEventPartials<AppDistributionEvent<
14+
export const alertsAppDistributionOnNewTesterIosDevicePublished: MockCloudEventAbstractFactory<AppDistributionEvent<
1015
NewTesterDevicePayload
1116
>> = {
12-
generatePartial(
13-
_: CloudFunction<AppDistributionEvent<NewTesterDevicePayload>>
14-
): DeepPartial<AppDistributionEvent<NewTesterDevicePayload>> {
17+
generateMock(
18+
cloudFunction: CloudFunction<AppDistributionEvent<NewTesterDevicePayload>>
19+
): AppDistributionEvent<NewTesterDevicePayload> {
1520
const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
1621
const now = new Date().toISOString();
1722

1823
return {
24+
// Spread common fields
25+
...getBaseCloudEvent(cloudFunction),
26+
// Spread fields specific to this CloudEvent
1927
source,
2028
data: {
2129
createTime: now,

src/cloudevent/partials/alerts/billing-on-plan-automated-update-published.ts renamed to src/cloudevent/mocks/alerts/billing-on-plan-automated-update-published.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
1-
import { DeepPartial, MockCloudEventPartials } from '../../types';
1+
import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
22
import { CloudFunction } from 'firebase-functions/v2';
33
import { FirebaseAlertData } from 'firebase-functions/v2/alerts';
44
import {
55
BillingEvent,
66
PlanAutomatedUpdatePayload,
77
} from 'firebase-functions/v2/alerts/billing';
8-
import { getEventFilters, getEventType, PROJECT_ID } from '../helpers';
8+
import {
9+
getBaseCloudEvent,
10+
getEventFilters,
11+
getEventType,
12+
PROJECT_ID,
13+
} from '../helpers';
914

10-
export const alertsBillingOnPlanAutomatedUpdatePublished: MockCloudEventPartials<BillingEvent<
15+
export const alertsBillingOnPlanAutomatedUpdatePublished: MockCloudEventAbstractFactory<BillingEvent<
1116
PlanAutomatedUpdatePayload
1217
>> = {
13-
generatePartial(
14-
_: CloudFunction<BillingEvent<PlanAutomatedUpdatePayload>>
15-
): DeepPartial<BillingEvent<PlanAutomatedUpdatePayload>> {
18+
generateMock(
19+
cloudFunction: CloudFunction<BillingEvent<PlanAutomatedUpdatePayload>>
20+
): BillingEvent<PlanAutomatedUpdatePayload> {
1621
const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
1722

1823
return {
24+
// Spread common fields
25+
...getBaseCloudEvent(cloudFunction),
26+
// Spread fields specific to this CloudEvent
1927
source,
2028
data: getBillingPlanAutomatedUpdateData(),
2129
};

src/cloudevent/partials/alerts/billing-on-plan-update-published.ts renamed to src/cloudevent/mocks/alerts/billing-on-plan-update-published.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
1-
import { DeepPartial, MockCloudEventPartials } from '../../types';
1+
import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
22
import { CloudFunction } from 'firebase-functions/v2';
33
import { FirebaseAlertData } from 'firebase-functions/v2/alerts';
44
import {
55
BillingEvent,
66
PlanUpdatePayload,
77
} from 'firebase-functions/v2/alerts/billing';
8-
import { getEventFilters, getEventType, PROJECT_ID } from '../helpers';
8+
import {
9+
getBaseCloudEvent,
10+
getEventFilters,
11+
getEventType,
12+
PROJECT_ID,
13+
} from '../helpers';
914

10-
export const alertsBillingOnPlanUpdatePublished: MockCloudEventPartials<BillingEvent<
15+
export const alertsBillingOnPlanUpdatePublished: MockCloudEventAbstractFactory<BillingEvent<
1116
PlanUpdatePayload
1217
>> = {
13-
generatePartial(
14-
_: CloudFunction<BillingEvent<PlanUpdatePayload>>
15-
): DeepPartial<BillingEvent<PlanUpdatePayload>> {
18+
generateMock(
19+
cloudFunction: CloudFunction<BillingEvent<PlanUpdatePayload>>
20+
): BillingEvent<PlanUpdatePayload> {
1621
const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
1722

1823
return {
24+
// Spread common fields
25+
...getBaseCloudEvent(cloudFunction),
26+
// Spread fields specific to this CloudEvent
1927
source,
2028
data: getBillingPlanUpdateData(),
2129
};

src/cloudevent/partials/alerts/crashlytics-on-new-anr-issue-published.ts renamed to src/cloudevent/mocks/alerts/crashlytics-on-new-anr-issue-published.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
1-
import { DeepPartial, MockCloudEventPartials } from '../../types';
1+
import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
22
import { CloudFunction } from 'firebase-functions/v2';
3-
import { getEventFilters, getEventType, PROJECT_ID } from '../helpers';
3+
import {
4+
getBaseCloudEvent,
5+
getEventFilters,
6+
getEventType,
7+
PROJECT_ID,
8+
} from '../helpers';
49
import {
510
CrashlyticsEvent,
611
NewAnrIssuePayload,
712
} from 'firebase-functions/v2/alerts/crashlytics';
813
import { FirebaseAlertData } from 'firebase-functions/v2/alerts';
914

10-
export const alertsCrashlyticsOnNewAnrIssuePublished: MockCloudEventPartials<CrashlyticsEvent<
15+
export const alertsCrashlyticsOnNewAnrIssuePublished: MockCloudEventAbstractFactory<CrashlyticsEvent<
1116
NewAnrIssuePayload
1217
>> = {
13-
generatePartial(
14-
_: CloudFunction<CrashlyticsEvent<NewAnrIssuePayload>>
15-
): DeepPartial<CrashlyticsEvent<NewAnrIssuePayload>> {
18+
generateMock(
19+
cloudFunction: CloudFunction<CrashlyticsEvent<NewAnrIssuePayload>>
20+
): CrashlyticsEvent<NewAnrIssuePayload> {
1621
const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
1722

1823
return {
24+
// Spread common fields
25+
...getBaseCloudEvent(cloudFunction),
26+
// Spread fields specific to this CloudEvent
1927
source,
2028
data: getCrashlyticsNewAnrIssueData(),
2129
};

0 commit comments

Comments
 (0)