Skip to content

Commit 6006808

Browse files
committed
working tests
1 parent 054d856 commit 6006808

File tree

8 files changed

+238
-71
lines changed

8 files changed

+238
-71
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ firebase-functions-*.tgz
1313
integration_test/.firebaserc
1414
integration_test/*.log
1515
integration_test/functions/firebase-functions.tgz
16-
integration_test/functions/package.json
1716
lib
1817
node_modules
1918
npm-debug.log

integration_test/cli.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -125,20 +125,12 @@ async function cleanupFunctions(codebase: string): Promise<void> {
125125
async function main(): Promise<void> {
126126
let success = false;
127127
try {
128-
// Step 1: Generate run ID (already done)
129-
// Step 2: Build and pack the SDK tarball
130128
await buildAndPackSDK();
131-
132-
// Step 3: Write firebase.json with codebase
133129
await writeFirebaseJson(runId);
134-
135130
await writeEnvFile(runId);
136-
137-
// Step 4: Deploy functions
138131
await deployFunctions(runId);
139-
140-
// Step 5: Wait (deployment already waits)
141-
// Step 6: Run tests
132+
console.log("Waiting 20 seconds for deployments fully provision before running tests...");
133+
await new Promise((resolve) => setTimeout(resolve, 20_000));
142134
await runTests(runId);
143135

144136
success = true;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "functions",
3+
"scripts": {
4+
"build": "tsc",
5+
"build:watch": "tsc --watch",
6+
"serve": "npm run build && firebase emulators:start --only functions",
7+
"shell": "npm run build && firebase functions:shell",
8+
"start": "npm run shell",
9+
"deploy": "firebase deploy --only functions",
10+
"logs": "firebase functions:log"
11+
},
12+
"engines": {
13+
"node": "22"
14+
},
15+
"main": "lib/index.js",
16+
"dependencies": {
17+
"@google-cloud/pubsub": "^5.2.0",
18+
"firebase-admin": "^12.6.0",
19+
"firebase-functions": "file:firebase-functions-local.tgz"
20+
},
21+
"devDependencies": {
22+
"firebase-functions-test": "^3.1.0",
23+
"typescript": "^5.7.3"
24+
},
25+
"private": true
26+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { expect, assertType } from "vitest";
2+
import { RUN_ID } from "./utils";
3+
4+
export function expectCloudEvent(data: any) {
5+
expect(data.specversion).toBe("1.0");
6+
expect(data.id).toBeDefined();
7+
assertType<string>(data.id);
8+
expect(data.id.length).toBeGreaterThan(0);
9+
expect(data.source).toBeDefined();
10+
assertType<string>(data.source);
11+
expect(data.source.length).toBeGreaterThan(0);
12+
expect(data.subject).toBeDefined();
13+
assertType<string>(data.subject);
14+
expect(data.subject.length).toBeGreaterThan(0);
15+
expect(data.type).toBeDefined();
16+
assertType<string>(data.type);
17+
expect(data.type.length).toBeGreaterThan(0);
18+
expect(data.time).toBeDefined();
19+
assertType<string>(data.time);
20+
expect(data.time.length).toBeGreaterThan(0);
21+
// iso string to unix - will be NaN if not a valid date
22+
expect(Date.parse(data.time)).toBeGreaterThan(0);
23+
}
24+
25+
export function expectFirestoreEvent(data: any, documentId: string) {
26+
expect(data.location).toBeDefined();
27+
assertType<string>(data.location);
28+
expect(data.location.length).toBeGreaterThan(0);
29+
expect(data.project).toBeDefined();
30+
assertType<string>(data.project);
31+
expect(data.project.length).toBeGreaterThan(0);
32+
expect(data.database).toBeDefined();
33+
assertType<string>(data.database);
34+
expect(data.database.length).toBeGreaterThan(0);
35+
expect(data.namespace).toBeDefined();
36+
assertType<string>(data.namespace);
37+
expect(data.namespace.length).toBeGreaterThan(0);
38+
expect(data.document).toBeDefined();
39+
assertType<string>(data.document);
40+
expect(data.document.length).toBeGreaterThan(0);
41+
expect(data.document).toBe(`integration_test/${RUN_ID}/${documentId}`);
42+
expect(data.params).toBeDefined();
43+
expect(data.params.runId).toBe(RUN_ID);
44+
expect(data.params.documentId).toBe(documentId);
45+
}

integration_test/functions/src/firestore.v2.test.ts

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, it, beforeAll, expect } from "vitest";
22
import { firestore } from "./utils";
33
import { GeoPoint } from "firebase-admin/firestore";
4+
import { expectCloudEvent, expectFirestoreEvent } from "./assertions";
45
const RUN_ID = String(process.env.RUN_ID);
56

67
function waitForEvent<T = unknown>(
@@ -11,14 +12,16 @@ function waitForEvent<T = unknown>(
1112
return new Promise<T>((resolve, reject) => {
1213
let timer: NodeJS.Timeout | null = null;
1314

14-
const unsubscribe = firestore.collection(`${RUN_ID}_snapshots`).doc(event).onSnapshot(snapshot => {
15-
if (snapshot.exists) {
16-
console.log("snapshot", snapshot.data());
17-
if (timer) clearTimeout(timer);
18-
unsubscribe();
19-
resolve(snapshot.data() as T);
20-
}
21-
});
15+
const unsubscribe = firestore
16+
.collection(RUN_ID)
17+
.doc(event)
18+
.onSnapshot((snapshot) => {
19+
if (snapshot.exists) {
20+
if (timer) clearTimeout(timer);
21+
unsubscribe();
22+
resolve(snapshot.data() as T);
23+
}
24+
});
2225

2326
timer = setTimeout(() => {
2427
unsubscribe();
@@ -32,41 +35,29 @@ function waitForEvent<T = unknown>(
3235
describe("firestore.v2", () => {
3336
describe("onDocumentCreated", () => {
3437
let data: any;
38+
let documentId: string;
3539

3640
beforeAll(async () => {
3741
data = await waitForEvent("onDocumentCreated", async () => {
38-
console.log("triggering event", RUN_ID);
3942
await firestore
40-
.collection(RUN_ID)
41-
.doc("onDocumentCreated")
42-
.set({
43+
.collection(`integration_test/${RUN_ID}/onDocumentCreated`)
44+
.add({
4345
foo: "bar",
4446
timestamp: new Date(),
4547
geopoint: new GeoPoint(10, 20),
46-
}).then(() => {
47-
console.log("event triggered", RUN_ID);
48+
})
49+
.then((doc) => {
50+
documentId = doc.id;
4851
});
4952
});
50-
console.log("data", data);
5153
});
5254

5355
it("should be a CloudEvent", () => {
54-
expect(data.specversion).toBe("1.0");
55-
expect(data.id).toBeDefined();
56-
expect(data.source).toBeDefined();
57-
expect(data.subject).toBeDefined();
58-
expect(data.type).toBeDefined();
59-
expect(data.time).toBeDefined(); // check its an iosdate
56+
expectCloudEvent(data);
6057
});
6158

6259
it("should be a FirestoreEvent", () => {
63-
expect(data.location).toBeDefined();
64-
expect(data.project).toBeDefined();
65-
expect(data.database).toBeDefined();
66-
expect(data.namespace).toBeDefined();
67-
expect(data.document).toBeDefined();
68-
expect(data.params).toBeDefined();
69-
expect(data.params.documentId).toBe("onDocumentCreated");
60+
expectFirestoreEvent(data, documentId);
7061
});
7162

7263
it("should be a QueryDocumentSnapshot", () => {
Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,18 @@
11
import { onDocumentCreated } from "firebase-functions/v2/firestore";
2-
import { RUN_ID, serializeData } from "./utils";
3-
import { logger } from "firebase-functions";
2+
import { RUN_ID } from "./utils";
43
import { firestore } from "./utils";
4+
import { serializeFirestoreEvent, serializeQueryDocumentSnapshot } from "./serializers";
55

66
async function sendEvent(event: string, data: any): Promise<void> {
7-
await firestore.collection(`${RUN_ID}_snapshots`).doc(event).set(serializeData(data));
7+
await firestore.collection(RUN_ID).doc(event).set(data);
88
}
99

1010
export const firestoreOnDocumentCreatedTrigger = onDocumentCreated(
11-
`${RUN_ID}/{documentId}`,
11+
`integration_test/{runId}/onDocumentCreated/{documentId}`,
1212
async (event) => {
13-
logger.debug("onDocumentCreated", event);
14-
await sendEvent("onDocumentCreated", event);
13+
await sendEvent(
14+
"onDocumentCreated",
15+
serializeFirestoreEvent(event, serializeQueryDocumentSnapshot(event.data!))
16+
);
1517
}
1618
);
17-
18-
19-
export const foo = onDocumentCreated(
20-
`test/{documentId}`,
21-
async (event) => {
22-
logger.debug("onDocumentCreated", event);
23-
await sendEvent("onDocumentCreated", event);
24-
}
25-
);
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { DocumentData, DocumentReference, DocumentSnapshot, GeoPoint, QuerySnapshot, Timestamp } from "firebase-admin/firestore";
2+
import { CloudEvent } from "firebase-functions";
3+
import { FirestoreEvent, QueryDocumentSnapshot } from "firebase-functions/firestore";
4+
5+
export function serializeCloudEvent(event: CloudEvent<unknown>): any {
6+
return {
7+
specversion: event.specversion,
8+
id: event.id,
9+
source: event.source,
10+
subject: event.subject,
11+
type: event.type,
12+
time: event.time,
13+
};
14+
}
15+
16+
export function serializeFirestoreEvent(event: FirestoreEvent<unknown>, data: any): any {
17+
return {
18+
...serializeCloudEvent(event),
19+
location: event.location,
20+
project: event.project,
21+
database: event.database,
22+
namespace: event.namespace,
23+
document: event.document,
24+
params: event.params,
25+
data,
26+
};
27+
}
28+
29+
export function serializeQuerySnapshot(snapshot: QuerySnapshot): any {
30+
return {
31+
docs: snapshot.docs.map(serializeQueryDocumentSnapshot),
32+
};
33+
}
34+
35+
export function serializeQueryDocumentSnapshot(snapshot: QueryDocumentSnapshot): any {
36+
return serializeDocumentSnapshot(snapshot);
37+
}
38+
39+
export function serializeDocumentSnapshot(snapshot: DocumentSnapshot): any {
40+
return {
41+
exists: snapshot.exists,
42+
ref: serializeDocumentReference(snapshot.ref),
43+
id: snapshot.id,
44+
createTime: serializeTimestamp(snapshot.createTime),
45+
updateTime: serializeTimestamp(snapshot.updateTime),
46+
data: serializeDocumentData(snapshot.data() ?? {}),
47+
};
48+
}
49+
50+
export function serializeGeoPoint(geoPoint: GeoPoint): any {
51+
return {
52+
_type: "geopoint",
53+
latitude: geoPoint.latitude,
54+
longitude: geoPoint.longitude,
55+
};
56+
}
57+
58+
export function serializeTimestamp(timestamp?: Timestamp): any {
59+
if (!timestamp) {
60+
return null;
61+
}
62+
63+
return {
64+
_type: "timestamp",
65+
seconds: timestamp.seconds,
66+
nanoseconds: timestamp.nanoseconds,
67+
iso: timestamp.toDate().toISOString(),
68+
};
69+
}
70+
71+
export function serializeDocumentReference(reference: DocumentReference): any {
72+
return {
73+
_type: "reference",
74+
path: reference.path,
75+
id: reference.id,
76+
};
77+
}
78+
79+
function serializeDocumentData(data: DocumentData): any {
80+
const result: Record<string, unknown> = {};
81+
for (const [key, value] of Object.entries(data)) {
82+
if (value instanceof Timestamp) {
83+
result[key] = serializeTimestamp(value);
84+
} else if (value instanceof GeoPoint) {
85+
result[key] = serializeGeoPoint(value);
86+
} else if (value instanceof DocumentReference) {
87+
result[key] = serializeDocumentReference(value);
88+
} else if (Array.isArray(value)) {
89+
result[key] = value.map(serializeDocumentData);
90+
} else if (typeof value === "object" && value !== null) {
91+
result[key] = serializeDocumentData(value);
92+
} else {
93+
result[key] = value;
94+
}
95+
}
96+
return result;
97+
}

0 commit comments

Comments
 (0)