Skip to content

Commit e67996b

Browse files
committed
add storage tests
1 parent 9844cfd commit e67996b

File tree

7 files changed

+227
-0
lines changed

7 files changed

+227
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { assertType, expect } from "vitest";
2+
import { config } from "../config";
3+
4+
export function expectStorageObjectData(data: any, filename: string) {
5+
expect(data.bucket).toBe(config.storageBucket);
6+
expect(data.contentType).toBe("text/plain");
7+
8+
expect(data.crc32c).toBeDefined();
9+
assertType<string>(data.crc32c);
10+
expect(data.crc32c.length).toBeGreaterThan(0);
11+
12+
expect(data.md5Hash).toBeDefined();
13+
assertType<string>(data.md5Hash);
14+
expect(data.md5Hash.length).toBeGreaterThan(0);
15+
16+
expect(data.etag).toBeDefined();
17+
assertType<string>(data.etag);
18+
expect(data.etag.length).toBeGreaterThan(0);
19+
20+
expect(Number.parseInt(data.generation)).toBeGreaterThan(0);
21+
22+
expect(data.id).toBeDefined();
23+
assertType<string>(data.id);
24+
expect(data.id).toContain(config.storageBucket);
25+
expect(data.id).toContain(filename);
26+
27+
expect(data.kind).toBe("storage#object");
28+
29+
expect(data.mediaLink).toContain(`https://storage.googleapis.com/download/storage/v1/b/${config.storageBucket}/o/${filename}`);
30+
31+
expect(Number.parseInt(data.metageneration)).toBeGreaterThan(0);
32+
33+
expect(data.name).toBe(filename);
34+
35+
expect(data.selfLink).toBe(`https://www.googleapis.com/storage/v1/b/${config.storageBucket}/o/${filename}`);
36+
37+
expect(Number.parseInt(data.size)).toBeGreaterThan(0);
38+
39+
expect(data.storageClass).toBe("REGIONAL");
40+
41+
expect(Date.parse(data.timeCreated)).toBeGreaterThan(0);
42+
expect(Date.parse(data.timeStorageClassUpdated)).toBeGreaterThan(0);
43+
expect(Date.parse(data.updated)).toBeGreaterThan(0);
44+
}

integration_test/functions/src/firebase.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getAuth } from "firebase-admin/auth";
77
import { getRemoteConfig } from "firebase-admin/remote-config";
88
import { getFirestore } from "firebase-admin/firestore";
99
import { config } from "./config";
10+
import { getStorage } from "firebase-admin/storage";
1011

1112
export const app = admin.initializeApp({
1213
credential: applicationDefault(),
@@ -20,6 +21,7 @@ export const database = getDatabase(app);
2021
export const auth = getAuth(app);
2122
export const remoteConfig = getRemoteConfig(app);
2223
export const functions = getFunctions(app);
24+
export const storage = getStorage(app);
2325

2426
// See https://github.com/firebase/functions-samples/blob/a6ae4cbd3cf2fff3e2b97538081140ad9befd5d8/Node/taskqueues-backup-images/functions/index.js#L111-L128
2527
export async function getFunctionUrl(name: string) {

integration_test/functions/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export * from "./v2/identity.v2";
66
export * from "./v2/pubsub.v2";
77
export * from "./v2/remoteConfig.v2";
88
export * from "./v2/scheduler.v2";
9+
export * from "./v2/storage.v2";
910
export * from "./v2/tasks.v2";
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { serializeCloudEvent } from ".";
2+
import { StorageEvent, StorageObjectData } from "firebase-functions/v2/storage";
3+
4+
export function serializeStorageEvent(event: StorageEvent): any {
5+
return {
6+
...serializeCloudEvent(event),
7+
bucket: event.bucket, // Exposed at top-level and object level
8+
object: serializeStorageObjectData(event.data),
9+
};
10+
}
11+
12+
function serializeStorageObjectData(data: StorageObjectData): any {
13+
return {
14+
bucket: data.bucket,
15+
cacheControl: data.cacheControl,
16+
componentCount: data.componentCount,
17+
contentDisposition: data.contentDisposition,
18+
contentEncoding: data.contentEncoding,
19+
contentLanguage: data.contentLanguage,
20+
contentType: data.contentType,
21+
crc32c: data.crc32c,
22+
customerEncryption: data.customerEncryption,
23+
etag: data.etag,
24+
generation: data.generation,
25+
id: data.id,
26+
kind: data.kind,
27+
md5Hash: data.md5Hash,
28+
mediaLink: data.mediaLink,
29+
metadata: data.metadata,
30+
metageneration: data.metageneration,
31+
name: data.name,
32+
selfLink: data.selfLink,
33+
size: data.size,
34+
storageClass: data.storageClass,
35+
timeCreated: data.timeCreated,
36+
timeDeleted: data.timeDeleted,
37+
timeStorageClassUpdated: data.timeStorageClassUpdated,
38+
updated: data.updated,
39+
};
40+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// import { sendEvent } from "../utils";
2+
// import { onMutationExecuted } from "firebase-functions/dataconnect";
3+
4+
// export const databaseOnValueCreated = onMutationExecuted(
5+
// {
6+
// ref: `integration_test/{runId}/onValueCreated/{timestamp}`,
7+
// },
8+
// async (event) => {
9+
// await sendEvent(
10+
// "onValueCreated",
11+
// serializeDatabaseEvent(event, serializeDataSnapshot(event.data!))
12+
// );
13+
// }
14+
// );
15+
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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 { expectCloudEvent } from "../assertions";
6+
import { expectStorageObjectData } from "../assertions/storage";
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.v2", () => {
20+
let createdFile: Awaited<ReturnType<typeof createDummyFile>>;
21+
let uploadedData: any;
22+
let metadataData: any;
23+
let deletedData: any;
24+
25+
// Since storage triggers are bucket wide, we perform all events at the top-level
26+
// in a specific order, then assert the values at the end.
27+
beforeAll(async () => {
28+
uploadedData = await waitForEvent("onObjectFinalized", async () => {
29+
createdFile = await createDummyFile();
30+
});
31+
32+
metadataData = await waitForEvent("onObjectMetadataUpdated", async () => {
33+
await bucket.file(createdFile.name).setMetadata({
34+
runId: RUN_ID,
35+
});
36+
});
37+
38+
deletedData = await waitForEvent("onObjectDeleted", async () => {
39+
await bucket.file(createdFile.name).delete();
40+
});
41+
}, 60_000);
42+
43+
afterAll(async () => {
44+
// Just in case the file wasn't deleted by the trigger if it failed.
45+
await bucket.file(createdFile.name).delete({
46+
ignoreNotFound: true,
47+
});
48+
});
49+
50+
describe("onObjectDeleted", () => {
51+
it("should be a CloudEvent", () => {
52+
expectCloudEvent(deletedData);
53+
});
54+
55+
it("should have the correct data", () => {
56+
expect(deletedData.bucket).toBe(config.storageBucket);
57+
expectStorageObjectData(deletedData.object, filename);
58+
});
59+
60+
// TODO: Doesn't seem to be sent by Google Cloud?
61+
it.skip('should contain a timeDeleted timestamp', () => {
62+
expect(deletedData.object.timeDeleted).toBeDefined();
63+
expect(Date.parse(deletedData.object.timeDeleted)).toBeGreaterThan(0);
64+
});
65+
});
66+
67+
describe("onObjectMetadataUpdated", () => {
68+
it("should be a CloudEvent", () => {
69+
expectCloudEvent(metadataData);
70+
});
71+
72+
it("should have the correct data", () => {
73+
expect(metadataData.bucket).toBe(config.storageBucket);
74+
expectStorageObjectData(metadataData.object, filename);
75+
});
76+
77+
// TODO: Doesn't seem to be sent by Google Cloud?
78+
it.skip('should have metadata', () => {
79+
expect(metadataData.metadata).toBeDefined();
80+
expect(metadataData.metadata.runId).toBe(RUN_ID);
81+
});
82+
});
83+
84+
describe("onObjectFinalized", () => {
85+
it("should be a CloudEvent", () => {
86+
expectCloudEvent(uploadedData);
87+
});
88+
89+
it("should have the correct data", () => {
90+
expect(uploadedData.bucket).toBe(config.storageBucket);
91+
expectStorageObjectData(uploadedData.object, filename);
92+
});
93+
94+
// TODO: Doesn't seem to be sent by Google Cloud?
95+
it.skip('should not have initial metadata', () => {
96+
expect(uploadedData.object.metadata).toBeDefined();
97+
expect(uploadedData.object.metadata.runId).not.toBeUndefined();
98+
});
99+
100+
// TODO: Doesn't seem to be sent by Google Cloud?
101+
it.skip('should contain a timeCreated timestamp', () => {
102+
expect(uploadedData.object.timeCreated).toBeDefined();
103+
expect(Date.parse(uploadedData.object.timeCreated)).toBeGreaterThan(0);
104+
});
105+
});
106+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {
2+
onObjectDeleted,
3+
onObjectFinalized,
4+
onObjectMetadataUpdated,
5+
} from "firebase-functions/v2/storage";
6+
import { sendEvent } from "../utils";
7+
import { serializeStorageEvent } from "../serializers/storage";
8+
9+
export const storageOnObjectDeletedTrigger = onObjectDeleted(async (event) => {
10+
await sendEvent("onObjectDeleted", serializeStorageEvent(event));
11+
});
12+
13+
export const storageOnObjectFinalizedTrigger = onObjectFinalized(async (event) => {
14+
await sendEvent("onObjectFinalized", serializeStorageEvent(event));
15+
});
16+
17+
export const storageOnObjectMetadataUpdatedTrigger = onObjectMetadataUpdated(async (event) => {
18+
await sendEvent("onObjectMetadataUpdated", serializeStorageEvent(event));
19+
});

0 commit comments

Comments
 (0)