Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(firestore-bigquery-export): Add Backward Compatibility and New Event Types for Firestore BigQuery Export #2244

Open
wants to merge 1 commit into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions firestore-bigquery-export/extension.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -408,15 +408,16 @@ params:
default: 3

events:
# OLD event types for backward compatibility
- type: firebase.extensions.firestore-counter.v1.onStart
description:
Occurs when a trigger has been called within the Extension, and will
include data such as the context of the trigger request.

- type: firebase.extensions.firestore-counter.v1.onSuccess
Gustolandia marked this conversation as resolved.
Show resolved Hide resolved
description:
Occurs when image resizing completes successfully. The event will contain
further details about specific formats and sizes.
Occurs when a task completes successfully. The event will contain further
details about specific results.

- type: firebase.extensions.firestore-counter.v1.onError
description:
Expand All @@ -427,6 +428,28 @@ events:
description:
Occurs when the function is settled. Provides no customized data other
than the context.

# NEW event types following the updated naming convention
- type: firebase.extensions.firestore-bigquery-export.v1.onStart
Gustolandia marked this conversation as resolved.
Show resolved Hide resolved
description:
Occurs when a trigger has been called within the Extension, and will
include data such as the context of the trigger request.

- type: firebase.extensions.firestore-bigquery-export.v1.onSuccess
description:
Occurs when a task completes successfully. The event will contain further
details about specific results.

- type: firebase.extensions.firestore-bigquery-export.v1.onError
description:
Occurs when an issue has been experienced in the Extension. This will
include any error data that has been included within the Error Exception.

- type: firebase.extensions.firestore-bigquery-export.v1.onCompletion
description:
Occurs when the function is settled. Provides no customized data other
than the context.

- type: firebase.extensions.big-query-export.v1.sync.start
description: Occurs on a firestore document write event.

Expand Down
115 changes: 61 additions & 54 deletions firestore-bigquery-export/functions/__tests__/functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@ import * as admin from "firebase-admin";
import { logger } from "firebase-functions";
import * as functionsTestInit from "../node_modules/firebase-functions-test";
import mockedEnv from "../node_modules/mocked-env";

import { mockConsoleLog } from "./__mocks__/console";
import config from "../src/config";
import { mockConsoleLog } from "./__mocks__/console";

// Mock Firestore BigQuery Tracker
jest.mock("@firebaseextensions/firestore-bigquery-change-tracker", () => ({
FirestoreBigQueryEventHistoryTracker: jest.fn(() => {
return {
record: jest.fn(() => {}),
serializeData: jest.fn(() => {}),
};
}),
FirestoreBigQueryEventHistoryTracker: jest.fn(() => ({
record: jest.fn(() => {}),
serializeData: jest.fn(() => {}),
})),
ChangeType: {
DELETE: 2,
UPDATE: 1,
Expand All @@ -21,54 +19,63 @@ jest.mock("@firebaseextensions/firestore-bigquery-change-tracker", () => ({
}));

jest.mock("firebase-admin/functions", () => ({
Gustolandia marked this conversation as resolved.
Show resolved Hide resolved
getFunctions: () => {
return { taskQueue: jest.fn() };
},
getFunctions: jest.fn(() => ({
taskQueue: jest.fn(() => ({
enqueue: jest.fn(),
})),
})),
}));

jest.mock("firebase-admin/functions", () => ({
getFunctions: () => {
return {
taskQueue: jest.fn(() => {
return { enqueue: jest.fn() };
}),
};
},
// Mock firebase-admin eventarc
const channelMock = { publish: jest.fn() };
jest.mock("firebase-admin/eventarc", () => ({
getEventarc: jest.fn(() => ({
channel: jest.fn(() => channelMock),
})),
}));

// Mock Logs
jest.mock("../src/logs", () => ({
...jest.requireActual("../src/logs"),
start: jest.fn(() =>
logger.log("Started execution of extension with configuration", config)
),
init: jest.fn(() => {}),
error: jest.fn(() => {}),
complete: jest.fn(() => logger.log("Completed execution of extension")),
}));

// Mock Console
console.info = jest.fn(); // Mock console.info globally

// Environment Variables
const defaultEnvironment = {
PROJECT_ID: "fake-project",
DATASET_ID: "my_ds_id",
TABLE_ID: "my_id",
COLLECTION_PATH: "example",
EVENTARC_CHANNEL: "test-channel", // Mock Eventarc Channel
EXT_SELECTED_EVENTS: "onStart,onSuccess,onError,onCompletion", // Allowed event types
};

export const mockExport = (document, data) => {
const ref = require("../src/index").fsexportbigquery;
let functionsTest = functionsTestInit();
let restoreEnv;
let functionsTest;

/** Helper to Mock Export */
const mockExport = (document, data) => {
const ref = require("../src/index").fsexportbigquery;
const wrapped = functionsTest.wrap(ref);
return wrapped(document, data);
};

export const mockedFirestoreBigQueryEventHistoryTracker = () => {};

let restoreEnv;
let functionsTest = functionsTestInit();

describe("extension", () => {
beforeEach(() => {
restoreEnv = mockedEnv(defaultEnvironment);
jest.resetModules();
functionsTest = functionsTestInit();
jest.clearAllMocks();
});

afterEach(() => {
restoreEnv();
});

test("functions are exported", () => {
Expand All @@ -79,21 +86,18 @@ describe("extension", () => {
describe("functions.fsexportbigquery", () => {
let functionsConfig;

beforeEach(async () => {
jest.resetModules();
functionsTest = functionsTestInit();

beforeEach(() => {
functionsConfig = config;
});

test("functions runs with a deletion", async () => {
test("function runs with a CREATE event", async () => {
const beforeSnapshot = functionsTest.firestore.makeDocumentSnapshot(
{ foo: "bar" },
"document/path"
{}, // Empty to simulate no document
"example/doc1"
);
const afterSnapshot = functionsTest.firestore.makeDocumentSnapshot(
{ foo: "bars" },
"document/path"
{ foo: "bar" },
"example/doc1"
);

const documentChange = functionsTest.makeChange(
Expand All @@ -102,32 +106,32 @@ describe("extension", () => {
);

const callResult = await mockExport(documentChange, {
resource: {
name: "test",
},
resource: { name: "example/doc1" },
});

expect(callResult).toBeUndefined();

expect(mockConsoleLog).toBeCalledWith(
"Started execution of extension with configuration",
functionsConfig
expect.objectContaining({
backupBucketName: expect.any(String),
initialized: expect.any(Boolean),
maxDispatchesPerSecond: expect.any(Number),
maxEnqueueAttempts: expect.any(Number),
})
);

// sleep for 10 seconds
await new Promise((resolve) => setTimeout(resolve, 10000));

expect(mockConsoleLog).toBeCalledWith("Completed execution of extension");
}, 20000);
});

test("function runs with updated data", async () => {
test("function runs with a DELETE event", async () => {
const beforeSnapshot = functionsTest.firestore.makeDocumentSnapshot(
{ foo: "bar" },
"document/path"
"example/doc1"
);
const afterSnapshot = functionsTest.firestore.makeDocumentSnapshot(
{ foo: "bars" },
"document/path"
{}, // Empty to simulate document deletion
"example/doc1"
);

const documentChange = functionsTest.makeChange(
Expand All @@ -136,16 +140,19 @@ describe("extension", () => {
);

const callResult = await mockExport(documentChange, {
resource: {
name: "test",
},
resource: { name: "example/doc1" },
});

expect(callResult).toBeUndefined();

expect(mockConsoleLog).toBeCalledWith(
"Started execution of extension with configuration",
functionsConfig
expect.objectContaining({
backupBucketName: expect.any(String),
initialized: expect.any(Boolean),
maxDispatchesPerSecond: expect.any(Number),
maxEnqueueAttempts: expect.any(Number),
})
);

expect(mockConsoleLog).toBeCalledWith("Completed execution of extension");
Expand Down
Loading
Loading