Skip to content

Commit 5cde364

Browse files
committed
add v1 tests
1 parent c61a736 commit 5cde364

21 files changed

+876
-151
lines changed

integration_test/functions/src/assertions/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Resource } from "firebase-functions/v1";
12
import { expect, assertType } from "vitest";
23

34
export function expectCloudEvent(data: any) {
@@ -24,4 +25,23 @@ export function expectCloudEvent(data: any) {
2425
expect(data.time.length).toBeGreaterThan(0);
2526
// iso string to unix - will be NaN if not a valid date
2627
expect(Date.parse(data.time)).toBeGreaterThan(0);
28+
}
29+
30+
export function expectEventContext(data: any) {
31+
expect(data.eventId).toBeDefined();
32+
assertType<string>(data.eventId);
33+
expect(data.eventId.length).toBeGreaterThan(0);
34+
expect(data.eventType).toBeDefined();
35+
assertType<string>(data.eventType);
36+
expect(data.eventType.length).toBeGreaterThan(0);
37+
expect(data.resource).toBeDefined();
38+
assertType<Resource>(data.resource);
39+
expect(data.resource.service).toBeDefined();
40+
expect(data.resource.name).toBeDefined();
41+
expect(data.timestamp).toBeDefined();
42+
assertType<string>(data.timestamp);
43+
expect(data.timestamp.length).toBeGreaterThan(0);
44+
expect(Date.parse(data.timestamp)).toBeGreaterThan(0);
45+
expect(data.params).toBeDefined();
46+
assertType<Record<string, string>>(data.params);
2747
}

integration_test/functions/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
export * from "./v1/database.v1";
2+
export * from "./v1/firestore.v1";
3+
export * from "./v1/https.v1";
4+
export * from "./v1/remoteConfig.v1";
5+
export * from "./v1/pubsub.v1";
6+
export * from "./v1/storage.v1";
7+
export * from "./v1/tasks.v1";
8+
19
export * from "./v2/database.v2";
210
export * from "./v2/eventarc.v2";
311
export * from "./v2/firestore.v2";
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { describe, it, beforeAll, expect } from "vitest";
2+
import { RUN_ID, waitForEvent } from "./utils";
3+
import { expectEventContext, expectCloudEvent } from "./assertions";
4+
import { remoteConfig } from "./firebase.server";
5+
6+
describe("remoteConfig", () => {
7+
describe("onConfigUpdated", () => {
8+
let v1Data: any;
9+
let v2Data: any;
10+
11+
beforeAll(async () => {
12+
// Create a shared trigger that only executes once
13+
let triggerPromise: Promise<void> | null = null;
14+
const getTrigger = () => {
15+
if (!triggerPromise) {
16+
triggerPromise = (async () => {
17+
const template = await remoteConfig.getTemplate();
18+
template.version.description = RUN_ID;
19+
await remoteConfig.validateTemplate(template);
20+
await remoteConfig.publishTemplate(template);
21+
})();
22+
}
23+
return triggerPromise;
24+
};
25+
26+
// Wait for both events in parallel, sharing the same trigger
27+
[v1Data, v2Data] = await Promise.all([
28+
waitForEvent("onConfigUpdatedV1", getTrigger),
29+
waitForEvent("onConfigUpdated", getTrigger),
30+
]);
31+
}, 60_000);
32+
33+
describe("v1", () => {
34+
it("should have EventContext", () => {
35+
expectEventContext(v1Data);
36+
});
37+
38+
it("should have the correct data", () => {
39+
expect(v1Data.update.versionNumber).toBeDefined();
40+
expect(v1Data.update.updateTime).toBeDefined();
41+
expect(v1Data.update.updateUser).toBeDefined();
42+
expect(v1Data.update.description).toBeDefined();
43+
expect(v1Data.update.description).toBe(RUN_ID);
44+
expect(v1Data.update.updateOrigin).toBeDefined();
45+
expect(v1Data.update.updateOrigin).toBe("ADMIN_SDK_NODE");
46+
expect(v1Data.update.updateType).toBeDefined();
47+
expect(v1Data.update.updateType).toBe("INCREMENTAL_UPDATE");
48+
// rollback source optional in v1
49+
});
50+
});
51+
52+
describe("v2", () => {
53+
it("should be a CloudEvent", () => {
54+
expectCloudEvent(v2Data);
55+
});
56+
57+
it("should have the correct data", () => {
58+
expect(v2Data.update.versionNumber).toBeDefined();
59+
expect(v2Data.update.updateTime).toBeDefined();
60+
expect(v2Data.update.updateUser).toBeDefined();
61+
expect(v2Data.update.description).toBeDefined();
62+
expect(v2Data.update.description).toBe(RUN_ID);
63+
expect(v2Data.update.updateOrigin).toBeDefined();
64+
expect(v2Data.update.updateOrigin).toBe("ADMIN_SDK_NODE");
65+
expect(v2Data.update.updateType).toBeDefined();
66+
expect(v2Data.update.updateType).toBe("INCREMENTAL_UPDATE");
67+
expect(v2Data.update.rollbackSource).toBeDefined();
68+
});
69+
});
70+
});
71+
});
72+

integration_test/functions/src/serializers/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { CloudEvent } from "firebase-functions";
2+
import { EventContext } from "firebase-functions/v1";
23

34
export function serializeCloudEvent(event: CloudEvent<unknown>): any {
45
return {
@@ -9,4 +10,17 @@ export function serializeCloudEvent(event: CloudEvent<unknown>): any {
910
type: event.type,
1011
time: event.time,
1112
};
13+
}
14+
15+
// v1
16+
export function serializeEventContext(ctx: EventContext): any {
17+
return {
18+
auth: ctx.auth,
19+
authType: ctx.authType,
20+
eventId: ctx.eventId,
21+
eventType: ctx.eventType,
22+
params: ctx.params,
23+
resource: ctx.resource,
24+
timestamp: ctx.timestamp,
25+
};
1226
}

integration_test/functions/src/serializers/storage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ function serializeStorageObjectData(data: StorageObjectData): any {
3838
updated: data.updated,
3939
};
4040
}
41+
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { describe, it, beforeAll, expect, afterAll } from "vitest";
2+
import { RUN_ID, waitForEvent } from "./utils";
3+
import { storage } from "./firebase.server";
4+
import { config } from "./config";
5+
import { expectStorageObjectData } from "./assertions/storage";
6+
import { expectEventContext, expectCloudEvent } from "./assertions";
7+
8+
const bucket = storage.bucket(config.storageBucket);
9+
const filename = `dummy-file-${RUN_ID}.txt`;
10+
11+
async function createDummyFile() {
12+
const buffer = Buffer.from("Hello, world!");
13+
const file = bucket.file(filename);
14+
await file.save(buffer);
15+
const [metadata] = await file.getMetadata();
16+
return metadata;
17+
}
18+
19+
describe("storage", () => {
20+
let createdFile: Awaited<ReturnType<typeof createDummyFile>>;
21+
let v1UploadedData: any;
22+
let v2UploadedData: any;
23+
let v1MetadataData: any;
24+
let v2MetadataData: any;
25+
let v1DeletedData: any;
26+
let v2DeletedData: any;
27+
28+
// Since storage triggers are bucket wide, we perform all events at the top-level
29+
// in a specific order, then assert the values at the end.
30+
beforeAll(async () => {
31+
// Create file - triggers both v1 and v2 onObjectFinalized
32+
let createFilePromise: Promise<void> | null = null;
33+
const getCreateFileTrigger = () => {
34+
if (!createFilePromise) {
35+
createFilePromise = (async () => {
36+
createdFile = await createDummyFile();
37+
})();
38+
}
39+
return createFilePromise;
40+
};
41+
42+
[v1UploadedData, v2UploadedData] = await Promise.all([
43+
waitForEvent("onObjectFinalizedV1", getCreateFileTrigger),
44+
waitForEvent("onObjectFinalized", getCreateFileTrigger),
45+
]);
46+
47+
// Update metadata - triggers both v1 and v2 onObjectMetadataUpdated
48+
let updateMetadataPromise: Promise<void> | null = null;
49+
const getUpdateMetadataTrigger = () => {
50+
if (!updateMetadataPromise) {
51+
updateMetadataPromise = (async () => {
52+
await bucket.file(createdFile.name).setMetadata({
53+
runId: RUN_ID,
54+
});
55+
})();
56+
}
57+
return updateMetadataPromise;
58+
};
59+
60+
[v1MetadataData, v2MetadataData] = await Promise.all([
61+
waitForEvent("onObjectMetadataUpdatedV1", getUpdateMetadataTrigger),
62+
waitForEvent("onObjectMetadataUpdated", getUpdateMetadataTrigger),
63+
]);
64+
65+
// Delete file - triggers both v1 and v2 onObjectDeleted
66+
let deleteFilePromise: Promise<void> | null = null;
67+
const getDeleteFileTrigger = () => {
68+
if (!deleteFilePromise) {
69+
deleteFilePromise = (async () => {
70+
await bucket.file(createdFile.name).delete();
71+
})();
72+
}
73+
return deleteFilePromise;
74+
};
75+
76+
[v1DeletedData, v2DeletedData] = await Promise.all([
77+
waitForEvent("onObjectDeletedV1", getDeleteFileTrigger),
78+
waitForEvent("onObjectDeleted", getDeleteFileTrigger),
79+
]);
80+
}, 60_000);
81+
82+
afterAll(async () => {
83+
// Just in case the file wasn't deleted by the trigger if it failed.
84+
await bucket.file(createdFile.name).delete({
85+
ignoreNotFound: true,
86+
});
87+
});
88+
89+
describe("onObjectDeleted", () => {
90+
describe("v1", () => {
91+
it("should have event context", () => {
92+
expectEventContext(v1DeletedData);
93+
});
94+
95+
it("should have the correct data", () => {
96+
expect(v1DeletedData.object.bucket).toBe(config.storageBucket);
97+
// Use the actual filename from the object data
98+
const actualFilename = v1DeletedData.object.name || filename;
99+
expectStorageObjectData(v1DeletedData.object, actualFilename);
100+
});
101+
102+
// TODO: Doesn't seem to be sent by Google Cloud?
103+
it.skip('should contain a timeDeleted timestamp', () => {
104+
expect(v1DeletedData.object.timeDeleted).toBeDefined();
105+
expect(Date.parse(v1DeletedData.object.timeDeleted)).toBeGreaterThan(0);
106+
});
107+
});
108+
109+
describe("v2", () => {
110+
it("should be a CloudEvent", () => {
111+
expectCloudEvent(v2DeletedData);
112+
});
113+
114+
it("should have the correct data", () => {
115+
expect(v2DeletedData.bucket).toBe(config.storageBucket);
116+
expectStorageObjectData(v2DeletedData.object, filename);
117+
});
118+
119+
// TODO: Doesn't seem to be sent by Google Cloud?
120+
it.skip('should contain a timeDeleted timestamp', () => {
121+
expect(v2DeletedData.object.timeDeleted).toBeDefined();
122+
expect(Date.parse(v2DeletedData.object.timeDeleted)).toBeGreaterThan(0);
123+
});
124+
});
125+
});
126+
127+
describe("onObjectMetadataUpdated", () => {
128+
describe("v1", () => {
129+
it("should have event context", () => {
130+
// Note: onObjectMetadataUpdated may not always have event context in v1
131+
if (v1MetadataData.eventId !== undefined) {
132+
expect(v1MetadataData.eventId).toBeDefined();
133+
expect(v1MetadataData.eventType).toBeDefined();
134+
expect(v1MetadataData.timestamp).toBeDefined();
135+
expect(v1MetadataData.resource).toBeDefined();
136+
}
137+
});
138+
139+
it("should have the correct data", () => {
140+
expect(v1MetadataData.object.bucket).toBe(config.storageBucket);
141+
// Use the actual filename from the object data
142+
const actualFilename = v1MetadataData.object.name || filename;
143+
expectStorageObjectData(v1MetadataData.object, actualFilename);
144+
});
145+
146+
// TODO: Doesn't seem to be sent by Google Cloud?
147+
it.skip('should have metadata', () => {
148+
expect(v1MetadataData.object.metadata).toBeDefined();
149+
expect(v1MetadataData.object.metadata.runId).toBe(RUN_ID);
150+
});
151+
});
152+
153+
describe("v2", () => {
154+
it("should be a CloudEvent", () => {
155+
expectCloudEvent(v2MetadataData);
156+
});
157+
158+
it("should have the correct data", () => {
159+
expect(v2MetadataData.bucket).toBe(config.storageBucket);
160+
expectStorageObjectData(v2MetadataData.object, filename);
161+
});
162+
163+
// TODO: Doesn't seem to be sent by Google Cloud?
164+
it.skip('should have metadata', () => {
165+
expect(v2MetadataData.metadata).toBeDefined();
166+
expect(v2MetadataData.metadata.runId).toBe(RUN_ID);
167+
});
168+
});
169+
});
170+
171+
describe("onObjectFinalized", () => {
172+
describe("v1", () => {
173+
it("should have event context", () => {
174+
expect(v1UploadedData.eventId).toBeDefined();
175+
expect(v1UploadedData.eventType).toBeDefined();
176+
expect(v1UploadedData.timestamp).toBeDefined();
177+
expect(v1UploadedData.resource).toBeDefined();
178+
});
179+
180+
it("should have the correct data", () => {
181+
expect(v1UploadedData.object.bucket).toBe(config.storageBucket);
182+
// Use the actual filename from the object data
183+
const actualFilename = v1UploadedData.object.name || filename;
184+
expectStorageObjectData(v1UploadedData.object, actualFilename);
185+
});
186+
187+
// TODO: Doesn't seem to be sent by Google Cloud?
188+
it.skip('should not have initial metadata', () => {
189+
expect(v1UploadedData.object.metadata).toBeDefined();
190+
expect(v1UploadedData.object.metadata.runId).not.toBeUndefined();
191+
});
192+
193+
// TODO: Doesn't seem to be sent by Google Cloud?
194+
it.skip('should contain a timeCreated timestamp', () => {
195+
expect(v1UploadedData.object.timeCreated).toBeDefined();
196+
expect(Date.parse(v1UploadedData.object.timeCreated)).toBeGreaterThan(0);
197+
});
198+
});
199+
200+
describe("v2", () => {
201+
it("should be a CloudEvent", () => {
202+
expectCloudEvent(v2UploadedData);
203+
});
204+
205+
it("should have the correct data", () => {
206+
expect(v2UploadedData.bucket).toBe(config.storageBucket);
207+
expectStorageObjectData(v2UploadedData.object, filename);
208+
});
209+
210+
// TODO: Doesn't seem to be sent by Google Cloud?
211+
it.skip('should not have initial metadata', () => {
212+
expect(v2UploadedData.object.metadata).toBeDefined();
213+
expect(v2UploadedData.object.metadata.runId).not.toBeUndefined();
214+
});
215+
216+
// TODO: Doesn't seem to be sent by Google Cloud?
217+
it.skip('should contain a timeCreated timestamp', () => {
218+
expect(v2UploadedData.object.timeCreated).toBeDefined();
219+
expect(Date.parse(v2UploadedData.object.timeCreated)).toBeGreaterThan(0);
220+
});
221+
});
222+
});
223+
});
224+

0 commit comments

Comments
 (0)