diff --git a/integration_test/README.md b/integration_test/README.md index 3b0f5413f..06a488fe9 100644 --- a/integration_test/README.md +++ b/integration_test/README.md @@ -8,7 +8,7 @@ Run the integration test as follows: ./run_tests.sh [] ``` -Test runs cycles of testing, once for Node.js 14 and another for Node.js 16. +Test runs cycles of testing, once for Node.js 18 and another for Node.js 20. Test uses locally installed firebase to invoke commands for deploying function. The test also requires that you have gcloud CLI installed and authenticated (`gcloud auth login`). @@ -16,6 +16,22 @@ gcloud CLI installed and authenticated (`gcloud auth login`). Integration test is triggered by invoking HTTP function integrationTest which in turns invokes each function trigger by issuing actions necessary to trigger it (e.g. write to storage bucket). +### Tested v2 Triggers + +The integration tests now cover all v2 SDK triggers: +- **HTTPS**: onCall (including streaming), onRequest +- **Database**: onValueWritten, onValueCreated, onValueDeleted, onValueUpdated +- **Firestore**: onDocumentWritten, onDocumentCreated, onDocumentDeleted, onDocumentUpdated +- **Storage**: onObjectFinalized, onObjectDeleted, onObjectArchived, onObjectMetadataUpdated +- **Pub/Sub**: onMessagePublished (with retry support) +- **Scheduled**: onSchedule +- **Tasks**: onTaskDispatched (with retry support) +- **Remote Config**: onConfigUpdated +- **Test Lab**: onTestMatrixCompleted +- **Identity**: beforeUserCreated, beforeUserSignedIn +- **Eventarc**: onCustomEventPublished +- **Alerts**: Crashlytics, Billing, App Distribution, Performance alerts + ### Debugging The status and result of each test is stored in RTDB of the project used for testing. You can also inspect Cloud Logging diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 623b690c7..e70c66d56 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -169,10 +169,77 @@ function v1Tests(testId: string, accessToken: string): Array> { function v2Tests(testId: string, accessToken: string): Array> { return [ // Invoke a callable HTTPS trigger. - // TODO: Temporarily disable - doesn't work unless running on projects w/ permission to create public functions. - // callV2HttpsTrigger("v2-callabletests", { foo: "bar", testId }, accessToken), + callV2HttpsTrigger("v2-callabletests", { foo: "bar", testId }, accessToken), + // Test streaming callable function + callV2HttpsTrigger("v2-callabletestsstreaming", { testId, streaming: true }, accessToken), + // Test onRequest endpoints + fetch( + `https://${REGION}-${firebaseConfig.projectId}.cloudfunctions.net/v2-httpstests?testId=${testId}` + ).then((r) => r.text()), // Invoke a scheduled trigger. callV2ScheduleTrigger("v2-schedule", "us-central1", accessToken), + // Database triggers + admin.database().ref(`v2tests/${testId}/db/written`).set("world"), + admin.database().ref(`v2tests/${testId}/db/created`).set("created"), + admin + .database() + .ref(`v2tests/${testId}/db/updated`) + .set("original") + .then(() => admin.database().ref(`v2tests/${testId}/db/updated`).set("updated")), + admin + .database() + .ref(`v2tests/${testId}/db/toDelete`) + .set("willBeDeleted") + .then(() => admin.database().ref(`v2tests/${testId}/db/toDelete`).remove()), + // Storage triggers + admin + .storage() + .bucket() + .upload(`/tmp/${testId}.txt`, { destination: `v2tests/${testId}/test.txt` }), + // Firestore triggers + admin + .firestore() + .collection(`v2tests/${testId}/firestore`) + .doc("test") + .set({ value: "written" }), + admin + .firestore() + .collection(`v2tests/${testId}/firestore-created`) + .doc("test") + .set({ value: "created" }), + admin + .firestore() + .collection(`v2tests/${testId}/firestore-updated`) + .doc("test") + .set({ value: "original" }) + .then(() => + admin + .firestore() + .collection(`v2tests/${testId}/firestore-updated`) + .doc("test") + .update({ value: "updated" }) + ), + admin + .firestore() + .collection(`v2tests/${testId}/firestore-to-delete`) + .doc("deleteMe") + .set({ value: "toBeDeleted" }) + .then(() => + admin + .firestore() + .collection(`v2tests/${testId}/firestore-to-delete`) + .doc("deleteMe") + .delete() + ), + // Pub/Sub triggers + new PubSub() + .topic("v2-pubsub-test-topic") + .publish(Buffer.from(JSON.stringify({ testId })), { testId }), + new PubSub() + .topic("v2-pubsub-test-retry-topic") + .publish(Buffer.from(JSON.stringify({ testRetry: true })), { testId, attempt: "1" }), + // Remote Config trigger + updateRemoteConfig(`v2-${testId}`, accessToken), ]; } diff --git a/integration_test/functions/src/v2/alerts-tests.ts b/integration_test/functions/src/v2/alerts-tests.ts new file mode 100644 index 000000000..2eec019bc --- /dev/null +++ b/integration_test/functions/src/v2/alerts-tests.ts @@ -0,0 +1,107 @@ +import { + onAlertPublished, + crashlytics, + billing, + appDistribution, + performance, +} from "firebase-functions/v2/alerts"; +import { expectEq, TestSuite } from "../testing"; + +export const alertstestsCrashlytics = onAlertPublished( + { alertType: "crashlytics.newFatalIssue" }, + (event) => { + const testId = event.appId || "unknown"; + + return new TestSuite("alerts crashlytics onAlertPublished") + .it("should have alert type", () => { + expectEq(event.alertType, "crashlytics.newFatalIssue"); + }) + .it("should have app id", () => { + expectEq(typeof event.appId, "string"); + }) + .run(testId, event); + } +); + +export const alertstestsBilling = onAlertPublished({ alertType: "billing.planUpdate" }, (event) => { + const testId = event.appId || "unknown"; + + return new TestSuite("alerts billing onAlertPublished") + .it("should have alert type", () => { + expectEq(event.alertType, "billing.planUpdate"); + }) + .run(testId, event); +}); + +export const alertstestsAppDistribution = appDistribution.onNewTesterIosDevicePublished((event) => { + const testId = event.appId || "unknown"; + + return new TestSuite("alerts appDistribution onNewTesterIosDevicePublished") + .it("should have app id", () => { + expectEq(typeof event.appId, "string"); + }) + .it("should have tester name", () => { + expectEq(typeof event.data.testerName, "string"); + }) + .it("should have device model", () => { + expectEq(typeof event.data.testerDeviceModelName, "string"); + }) + .it("should have device identifier", () => { + expectEq(typeof event.data.testerDeviceIdentifier, "string"); + }) + .run(testId, event); +}); + +export const alertstestsPerformance = performance.onThresholdAlertPublished((event) => { + const testId = event.appId || "unknown"; + + return new TestSuite("alerts performance onThresholdAlertPublished") + .it("should have app id", () => { + expectEq(typeof event.appId, "string"); + }) + .it("should have metric type", () => { + expectEq(typeof event.data.metricType, "string"); + }) + .it("should have event name", () => { + expectEq(typeof event.data.eventName, "string"); + }) + .it("should have threshold value", () => { + expectEq(typeof event.data.thresholdValue, "number"); + }) + .it("should have threshold unit", () => { + expectEq(typeof event.data.thresholdUnit, "string"); + }) + .run(testId, event); +}); + +export const alertstestsCrashlyticsRegression = crashlytics.onRegressionAlertPublished((event) => { + const testId = event.appId || "unknown"; + + return new TestSuite("alerts crashlytics onRegressionAlertPublished") + .it("should have app id", () => { + expectEq(typeof event.appId, "string"); + }) + .it("should have issue data", () => { + expectEq(typeof event.data.issue, "object"); + }) + .it("should have issue id", () => { + expectEq(typeof event.data.issue.id, "string"); + }) + .it("should have issue title", () => { + expectEq(typeof event.data.issue.title, "string"); + }) + .run(testId, event); +}); + +export const alertstestsBillingAutomated = billing.onPlanAutomatedUpdatePublished((event) => { + const testId = event.appId || "unknown"; + + return new TestSuite("alerts billing onPlanAutomatedUpdatePublished") + .it("should have billing plan", () => { + expectEq(typeof event.data.billingPlan, "string"); + }) + .it("should have notification type", () => { + expectEq(typeof event.data.notificationType, "string"); + }) + .run(testId, event); +}); diff --git a/integration_test/functions/src/v2/database-tests.ts b/integration_test/functions/src/v2/database-tests.ts new file mode 100644 index 000000000..72c75a0f0 --- /dev/null +++ b/integration_test/functions/src/v2/database-tests.ts @@ -0,0 +1,65 @@ +import * as admin from "firebase-admin"; +import { + onValueWritten, + onValueCreated, + onValueDeleted, + onValueUpdated, +} from "firebase-functions/v2/database"; +import { expectEq, TestSuite } from "../testing"; + +export const databasetestsWritten = onValueWritten("/v2tests/{testId}/db/written", (event) => { + return new TestSuite("database onValueWritten") + .it("should have path", () => { + expectEq(event.params.testId, event.data.after.ref.parent?.parent?.key); + }) + .it("should have data", () => { + expectEq(event.data.after.val(), "world"); + }) + .it("should have before data", () => { + expectEq(event.data.before.val(), null); + }) + .run(event.params.testId, event.data); +}); + +export const databasetestsCreated = onValueCreated("/v2tests/{testId}/db/created", (event) => { + return new TestSuite("database onValueCreated") + .it("should have path", () => { + expectEq(event.params.testId, event.data.ref.parent?.parent?.key); + }) + .it("should have data", () => { + expectEq(event.data.val(), "created"); + }) + .run(event.params.testId, event.data); +}); + +export const databasetestsDeleted = onValueDeleted( + "/v2tests/{testId}/db/deleted", + async (event) => { + // First write a value to be deleted + const db = admin.database(); + await db.ref(`/v2tests/${event.params.testId}/db/toDelete`).set("willBeDeleted"); + + return new TestSuite("database onValueDeleted") + .it("should have path", () => { + expectEq(event.params.testId, event.data.ref.parent?.parent?.key); + }) + .it("should have previous data", () => { + expectEq(event.data.val(), "willBeDeleted"); + }) + .run(event.params.testId, event.data); + } +); + +export const databasetestsUpdated = onValueUpdated("/v2tests/{testId}/db/updated", (event) => { + return new TestSuite("database onValueUpdated") + .it("should have path", () => { + expectEq(event.params.testId, event.data.after.ref.parent?.parent?.key); + }) + .it("should have new data", () => { + expectEq(event.data.after.val(), "updated"); + }) + .it("should have old data", () => { + expectEq(event.data.before.val(), "original"); + }) + .run(event.params.testId, event.data); +}); diff --git a/integration_test/functions/src/v2/eventarc-tests.ts b/integration_test/functions/src/v2/eventarc-tests.ts new file mode 100644 index 000000000..9942c3282 --- /dev/null +++ b/integration_test/functions/src/v2/eventarc-tests.ts @@ -0,0 +1,52 @@ +import { onCustomEventPublished } from "firebase-functions/v2/eventarc"; +import { expectEq, TestSuite } from "../testing"; + +export const eventarctests = onCustomEventPublished("test.v2.custom.event", (event) => { + const testId = event.data?.testId || "unknown"; + + return new TestSuite("eventarc onCustomEventPublished") + .it("should have event type", () => { + expectEq(event.type, "test.v2.custom.event"); + }) + .it("should have event id", () => { + expectEq(typeof event.id, "string"); + }) + .it("should have event source", () => { + expectEq(typeof event.source, "string"); + }) + .it("should have event time", () => { + expectEq(typeof event.time, "string"); + }) + .it("should have event data", () => { + expectEq(event.data?.message, "Hello from Eventarc"); + }) + .it("should have custom attributes", () => { + expectEq(event.data?.customAttribute, "customValue"); + }) + .run(testId, event); +}); + +export const eventarctestsWithFilter = onCustomEventPublished( + { + eventType: "test.v2.filtered.event", + channel: "locations/us-central1/channels/firebase", + filters: { + attribute1: "value1", + }, + }, + (event) => { + const testId = event.data?.testId || "unknown"; + + return new TestSuite("eventarc onCustomEventPublished with filters") + .it("should have matching event type", () => { + expectEq(event.type, "test.v2.filtered.event"); + }) + .it("should have filtered attribute", () => { + expectEq(event.data?.attribute1, "value1"); + }) + .it("should have channel data", () => { + expectEq(typeof event.source, "string"); + }) + .run(testId, event); + } +); diff --git a/integration_test/functions/src/v2/firestore-tests.ts b/integration_test/functions/src/v2/firestore-tests.ts new file mode 100644 index 000000000..22c492834 --- /dev/null +++ b/integration_test/functions/src/v2/firestore-tests.ts @@ -0,0 +1,81 @@ +import * as admin from "firebase-admin"; +import { + onDocumentWritten, + onDocumentCreated, + onDocumentDeleted, + onDocumentUpdated, +} from "firebase-functions/v2/firestore"; +import { expectEq, TestSuite } from "../testing"; + +export const firestoretestsWritten = onDocumentWritten( + "v2tests/{testId}/firestore/{docId}", + (event) => { + return new TestSuite("firestore onDocumentWritten") + .it("should have params", () => { + expectEq(typeof event.params.testId, "string"); + expectEq(typeof event.params.docId, "string"); + }) + .it("should have data after", () => { + expectEq(event.data?.after.data()?.value, "written"); + }) + .it("should have null data before for new docs", () => { + expectEq(event.data?.before.data(), undefined); + }) + .run(event.params.testId, event.data); + } +); + +export const firestoretestsCreated = onDocumentCreated( + "v2tests/{testId}/firestore-created/{docId}", + (event) => { + return new TestSuite("firestore onDocumentCreated") + .it("should have params", () => { + expectEq(typeof event.params.testId, "string"); + expectEq(typeof event.params.docId, "string"); + }) + .it("should have data", () => { + expectEq(event.data?.data()?.value, "created"); + }) + .run(event.params.testId, event.data); + } +); + +export const firestoretestsDeleted = onDocumentDeleted( + "v2tests/{testId}/firestore-deleted/{docId}", + async (event) => { + const db = admin.firestore(); + // Create a document to be deleted + await db + .collection(`v2tests/${event.params.testId}/firestore-to-delete`) + .doc("deleteMe") + .set({ value: "toBeDeleted" }); + + return new TestSuite("firestore onDocumentDeleted") + .it("should have params", () => { + expectEq(typeof event.params.testId, "string"); + expectEq(typeof event.params.docId, "string"); + }) + .it("should have previous data", () => { + expectEq(event.data?.data()?.value, "toBeDeleted"); + }) + .run(event.params.testId, event.data); + } +); + +export const firestoretestsUpdated = onDocumentUpdated( + "v2tests/{testId}/firestore-updated/{docId}", + (event) => { + return new TestSuite("firestore onDocumentUpdated") + .it("should have params", () => { + expectEq(typeof event.params.testId, "string"); + expectEq(typeof event.params.docId, "string"); + }) + .it("should have data after", () => { + expectEq(event.data?.after.data()?.value, "updated"); + }) + .it("should have data before", () => { + expectEq(event.data?.before.data()?.value, "original"); + }) + .run(event.params.testId, event.data); + } +); diff --git a/integration_test/functions/src/v2/https-tests.ts b/integration_test/functions/src/v2/https-tests.ts index b787ac602..72c1494ec 100644 --- a/integration_test/functions/src/v2/https-tests.ts +++ b/integration_test/functions/src/v2/https-tests.ts @@ -1,4 +1,4 @@ -import { onCall } from "firebase-functions/v2/https"; +import { onCall, onRequest } from "firebase-functions/v2/https"; import { expectEq, TestSuite } from "../testing"; export const callabletests = onCall({ invoker: "private" }, (req) => { @@ -6,3 +6,83 @@ export const callabletests = onCall({ invoker: "private" }, (req) => { .it("should have the correct data", (data: any) => expectEq(data?.foo, "bar")) .run(req.data.testId, req.data); }); + +export const callabletestsStreaming = onCall( + { + invoker: "private", + cors: true, + }, + async (req, res) => { + const testId = req.data.testId; + + // Check if client accepts streaming + if (req.acceptsStreaming) { + // Send multiple chunks + await res.sendChunk({ chunk: 1, message: "First chunk" }); + await res.sendChunk({ chunk: 2, message: "Second chunk" }); + await res.sendChunk({ chunk: 3, message: "Third chunk" }); + + return new TestSuite("v2 https onCall streaming") + .it("should support streaming", () => expectEq(req.acceptsStreaming, true)) + .it("should have test data", () => expectEq(typeof testId, "string")) + .run(testId, { streaming: true, chunks: 3 }); + } else { + return new TestSuite("v2 https onCall non-streaming fallback") + .it("should work without streaming", () => expectEq(req.acceptsStreaming, false)) + .run(testId, { streaming: false }); + } + } +); + +export const httpstests = onRequest( + { + cors: true, + maxInstances: 10, + }, + (req, res) => { + const testId = (req.query.testId as string) || "unknown"; + + new TestSuite("v2 https onRequest") + .it("should have request method", () => expectEq(typeof req.method, "string")) + .it("should have request url", () => expectEq(typeof req.url, "string")) + .it("should have request headers", () => expectEq(typeof req.headers, "object")) + .it("should support GET requests", () => { + if (req.method === "GET") { + expectEq(req.method, "GET"); + } + }) + .it("should support POST requests with body", () => { + if (req.method === "POST") { + expectEq(typeof req.body, "object"); + } + }) + .run(testId, req); + + res.status(200).json({ + success: true, + method: req.method, + testId: testId, + }); + } +); + +export const httpstestsAuth = onRequest( + { + invoker: "private", + }, + (req, res) => { + const testId = (req.query.testId as string) || "unknown"; + + new TestSuite("v2 https onRequest with auth") + .it("should have authorization header", () => { + expectEq(typeof req.headers.authorization, "string"); + }) + .run(testId, req); + + res.status(200).json({ + success: true, + authenticated: true, + testId: testId, + }); + } +); diff --git a/integration_test/functions/src/v2/identity-tests.ts b/integration_test/functions/src/v2/identity-tests.ts new file mode 100644 index 000000000..f218ee398 --- /dev/null +++ b/integration_test/functions/src/v2/identity-tests.ts @@ -0,0 +1,63 @@ +import { beforeUserCreated, beforeUserSignedIn } from "firebase-functions/v2/identity"; +import { expectEq, TestSuite } from "../testing"; + +export const identitytestsBeforeCreate = beforeUserCreated((event) => { + const testId = event.data.uid || "unknown"; + + new TestSuite("identity beforeUserCreated") + .it("should have user data", () => { + expectEq(typeof event.data.uid, "string"); + expectEq(typeof event.data.email, "string"); + }) + .it("should have event id", () => { + expectEq(typeof event.eventId, "string"); + }) + .it("should have timestamp", () => { + expectEq(typeof event.timestamp, "string"); + }) + .it("should have ip address", () => { + expectEq(typeof event.ipAddress, "string"); + }) + .it("should have user agent", () => { + expectEq(typeof event.userAgent, "string"); + }) + .run(testId, event); + + // Can modify user data + return { + displayName: event.data.displayName || "Test User", + disabled: false, + }; +}); + +export const identitytestsBeforeSignIn = beforeUserSignedIn((event) => { + const testId = event.data.uid || "unknown"; + + new TestSuite("identity beforeUserSignedIn") + .it("should have user data", () => { + expectEq(typeof event.data.uid, "string"); + }) + .it("should have event id", () => { + expectEq(typeof event.eventId, "string"); + }) + .it("should have timestamp", () => { + expectEq(typeof event.timestamp, "string"); + }) + .it("should have ip address", () => { + expectEq(typeof event.ipAddress, "string"); + }) + .it("should have user agent", () => { + expectEq(typeof event.userAgent, "string"); + }) + .it("should have additional user info if available", () => { + if (event.additionalUserInfo) { + expectEq(typeof event.additionalUserInfo.providerId, "string"); + } + }) + .run(testId, event); + + // Can block sign in by throwing + if (event.data.email?.includes("blocked")) { + throw new Error("Sign in blocked for test"); + } +}); diff --git a/integration_test/functions/src/v2/index.ts b/integration_test/functions/src/v2/index.ts index 38cde5f92..29ad501c9 100644 --- a/integration_test/functions/src/v2/index.ts +++ b/integration_test/functions/src/v2/index.ts @@ -2,6 +2,15 @@ import { setGlobalOptions } from "firebase-functions/v2"; import { REGION } from "../region"; setGlobalOptions({ region: REGION }); -// TODO: Temporarily disable - doesn't work unless running on projects w/ permission to create public functions. -// export * from './https-tests'; +export * from "./https-tests"; export * from "./scheduled-tests"; +export * from "./database-tests"; +export * from "./storage-tests"; +export * from "./firestore-tests"; +export * from "./pubsub-tests"; +export * from "./tasks-tests"; +export * from "./remoteConfig-tests"; +export * from "./testLab-tests"; +export * from "./identity-tests"; +export * from "./eventarc-tests"; +export * from "./alerts-tests"; diff --git a/integration_test/functions/src/v2/pubsub-tests.ts b/integration_test/functions/src/v2/pubsub-tests.ts new file mode 100644 index 000000000..9f96967a9 --- /dev/null +++ b/integration_test/functions/src/v2/pubsub-tests.ts @@ -0,0 +1,46 @@ +import { onMessagePublished } from "firebase-functions/v2/pubsub"; +import { expectEq, TestSuite } from "../testing"; + +export const pubsubtests = onMessagePublished("v2-pubsub-test-topic", (event) => { + return new TestSuite("pubsub onMessagePublished") + .it("should have message data", () => { + const data = JSON.parse(Buffer.from(event.data.message.data, "base64").toString()); + expectEq(data.testId, event.data.message.attributes?.testId); + }) + .it("should have message attributes", () => { + expectEq(typeof event.data.message.attributes?.testId, "string"); + }) + .it("should have message id", () => { + expectEq(typeof event.data.message.messageId, "string"); + }) + .it("should have publish time", () => { + expectEq(typeof event.data.message.publishTime, "string"); + }) + .run(event.data.message.attributes?.testId || "unknown", event.data); +}); + +export const pubsubtestsWithRetry = onMessagePublished( + { + topic: "v2-pubsub-test-retry-topic", + retry: true, + }, + (event) => { + const testId = event.data.message.attributes?.testId || "unknown"; + const attempt = parseInt(event.data.message.attributes?.attempt || "1"); + + // Fail on first attempt to test retry + if (attempt === 1) { + throw new Error("Intentional failure to test retry"); + } + + return new TestSuite("pubsub onMessagePublished with retry") + .it("should retry on failure", () => { + expectEq(attempt > 1, true); + }) + .it("should have message data", () => { + const data = JSON.parse(Buffer.from(event.data.message.data, "base64").toString()); + expectEq(data.testRetry, true); + }) + .run(testId, event.data); + } +); diff --git a/integration_test/functions/src/v2/remoteConfig-tests.ts b/integration_test/functions/src/v2/remoteConfig-tests.ts new file mode 100644 index 000000000..dfdf7f7b2 --- /dev/null +++ b/integration_test/functions/src/v2/remoteConfig-tests.ts @@ -0,0 +1,24 @@ +import { onConfigUpdated } from "firebase-functions/v2/remoteConfig"; +import { expectEq, TestSuite } from "../testing"; + +export const remoteconfigtests = onConfigUpdated((event) => { + const testId = event.data.versionNumber?.toString() || "unknown"; + + return new TestSuite("remoteConfig onConfigUpdated") + .it("should have version number", () => { + expectEq(typeof event.data.versionNumber, "string"); + }) + .it("should have update time", () => { + expectEq(typeof event.data.updateTime, "string"); + }) + .it("should have update user", () => { + expectEq(typeof event.data.updateUser?.email, "string"); + }) + .it("should have update origin", () => { + expectEq(typeof event.data.updateOrigin, "string"); + }) + .it("should have update type", () => { + expectEq(typeof event.data.updateType, "string"); + }) + .run(testId, event.data); +}); diff --git a/integration_test/functions/src/v2/storage-tests.ts b/integration_test/functions/src/v2/storage-tests.ts new file mode 100644 index 000000000..0c858ab42 --- /dev/null +++ b/integration_test/functions/src/v2/storage-tests.ts @@ -0,0 +1,65 @@ +import { + onObjectArchived, + onObjectDeleted, + onObjectFinalized, + onObjectMetadataUpdated, +} from "firebase-functions/v2/storage"; +import { expectEq, TestSuite } from "../testing"; + +const BUCKET_NAME = "gs://v2-storage-test-bucket"; + +export const storagetestsFinalized = onObjectFinalized({ bucket: BUCKET_NAME }, (event) => { + return new TestSuite("storage onObjectFinalized") + .it("should have object name", () => { + expectEq(typeof event.data.name, "string"); + }) + .it("should have bucket", () => { + expectEq(event.data.bucket, BUCKET_NAME.replace("gs://", "")); + }) + .it("should have size", () => { + expectEq(typeof event.data.size, "string"); + }) + .it("should have content type", () => { + expectEq(typeof event.data.contentType, "string"); + }) + .run(event.subject?.split("/").pop() || "unknown", event.data); +}); + +export const storagetestsDeleted = onObjectDeleted({ bucket: BUCKET_NAME }, (event) => { + return new TestSuite("storage onObjectDeleted") + .it("should have object name", () => { + expectEq(typeof event.data.name, "string"); + }) + .it("should have bucket", () => { + expectEq(event.data.bucket, BUCKET_NAME.replace("gs://", "")); + }) + .run(event.subject?.split("/").pop() || "unknown", event.data); +}); + +export const storagetestsArchived = onObjectArchived({ bucket: BUCKET_NAME }, (event) => { + return new TestSuite("storage onObjectArchived") + .it("should have object name", () => { + expectEq(typeof event.data.name, "string"); + }) + .it("should have bucket", () => { + expectEq(event.data.bucket, BUCKET_NAME.replace("gs://", "")); + }) + .run(event.subject?.split("/").pop() || "unknown", event.data); +}); + +export const storagetestsMetadataUpdated = onObjectMetadataUpdated( + { bucket: BUCKET_NAME }, + (event) => { + return new TestSuite("storage onObjectMetadataUpdated") + .it("should have object name", () => { + expectEq(typeof event.data.name, "string"); + }) + .it("should have bucket", () => { + expectEq(event.data.bucket, BUCKET_NAME.replace("gs://", "")); + }) + .it("should have metadata", () => { + expectEq(typeof event.data.metadata, "object"); + }) + .run(event.subject?.split("/").pop() || "unknown", event.data); + } +); diff --git a/integration_test/functions/src/v2/tasks-tests.ts b/integration_test/functions/src/v2/tasks-tests.ts new file mode 100644 index 000000000..c07912593 --- /dev/null +++ b/integration_test/functions/src/v2/tasks-tests.ts @@ -0,0 +1,57 @@ +import { onTaskDispatched } from "firebase-functions/v2/tasks"; +import { expectEq, TestSuite } from "../testing"; + +export const taskstests = onTaskDispatched( + { + retryConfig: { + maxAttempts: 3, + minBackoffSeconds: 1, + }, + rateLimits: { + maxConcurrentDispatches: 10, + }, + }, + (req) => { + const { testId, taskData } = req.data; + + return new TestSuite("tasks onTaskDispatched") + .it("should have task data", () => { + expectEq(taskData.message, "Hello from task queue"); + }) + .it("should have auth context if authenticated", () => { + if (req.auth) { + expectEq(typeof req.auth.uid, "string"); + } else { + expectEq(req.auth, undefined); + } + }) + .run(testId, req); + } +); + +export const taskstestsWithRetry = onTaskDispatched( + { + retryConfig: { + maxAttempts: 3, + minBackoffSeconds: 1, + maxBackoffSeconds: 10, + }, + }, + (req) => { + const { testId, attempt = 1 } = req.data; + + // Fail on first attempt to test retry + if (attempt === 1) { + throw new Error("Intentional failure to test retry"); + } + + return new TestSuite("tasks onTaskDispatched with retry") + .it("should retry on failure", () => { + expectEq(attempt > 1, true); + }) + .it("should have retry data", () => { + expectEq(req.data.testRetry, true); + }) + .run(testId, req); + } +); diff --git a/integration_test/functions/src/v2/testLab-tests.ts b/integration_test/functions/src/v2/testLab-tests.ts new file mode 100644 index 000000000..46393608b --- /dev/null +++ b/integration_test/functions/src/v2/testLab-tests.ts @@ -0,0 +1,31 @@ +import { onTestMatrixCompleted } from "firebase-functions/v2/testLab"; +import { expectEq, TestSuite } from "../testing"; + +export const testlabtests = onTestMatrixCompleted((event) => { + const testId = event.data.testMatrixId || "unknown"; + + return new TestSuite("testLab onTestMatrixCompleted") + .it("should have test matrix id", () => { + expectEq(typeof event.data.testMatrixId, "string"); + }) + .it("should have create time", () => { + expectEq(typeof event.data.createTime, "string"); + }) + .it("should have state", () => { + expectEq(typeof event.data.state, "string"); + }) + .it("should have outcome summary", () => { + expectEq(typeof event.data.outcomeSummary, "string"); + }) + .it("should have invalid matrix details if failed", () => { + if (event.data.state === "INVALID") { + expectEq(typeof event.data.invalidMatrixDetails, "string"); + } else { + expectEq(event.data.invalidMatrixDetails, undefined); + } + }) + .it("should have result storage", () => { + expectEq(typeof event.data.resultStorage.gcsPath, "string"); + }) + .run(testId, event.data); +}); diff --git a/integration_test/package.json.template b/integration_test/package.json.template index 42cdf121c..5e3b2108d 100644 --- a/integration_test/package.json.template +++ b/integration_test/package.json.template @@ -5,7 +5,7 @@ "build": "./node_modules/.bin/tsc" }, "dependencies": { - "@google-cloud/pubsub": "^2.10.0", + "@google-cloud/pubsub": "^4.0.0", "firebase-admin": "__FIREBASE_ADMIN__", "firebase-functions": "__SDK_TARBALL__", "node-fetch": "^2.6.7" @@ -13,7 +13,7 @@ "main": "lib/index.js", "devDependencies": { "@types/node-fetch": "^2.6.1", - "typescript": "^4.3.5" + "typescript": "^5.0.0" }, "engines": { "node": "__NODE_VERSION__" diff --git a/integration_test/run_tests.sh b/integration_test/run_tests.sh index 681d2dc1e..1d1dd2fdf 100755 --- a/integration_test/run_tests.sh +++ b/integration_test/run_tests.sh @@ -92,7 +92,7 @@ function cleanup { build_sdk delete_all_functions -for version in 14 16; do +for version in 18 20; do create_package_json $TIMESTAMP $version "^10.0.0" install_deps announce "Re-deploying the same functions to Node $version runtime ..."