From 64d6498ed0fd4e82914902363de26078030e1a1e Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Wed, 19 Aug 2026 00:04:53 +0100 Subject: [PATCH] fix(delete-user-data): resolve the RTDB client on first use getContext called admin.database() eagerly. With no RTDB instance configured there is no databaseURL to initialize the app with, so the call threw "Can't determine Firebase Database URL" and killed every invocation of clearData, handleSearch and handleDeletion, including for users who only delete Firestore data. Passing explicit options to initializeApp suppresses the FIREBASE_CONFIG fallback, so a deployed instance never picks up the project's default database URL either. Resolving the client on first use matches the extension, which calls admin.database() inside the RTDB deletion path. Adds an emulator codebase configured without an instance to cover the case, and asserts RTDB paths are still cleared when one is configured. --- kits/.gitignore | 2 +- kits/delete-user-data/firebase.json | 4 ++ kits/delete-user-data/src/index.ts | 6 +- .../tests/emulator/app-no-rtdb/.env | 15 +++++ .../tests/emulator/app-no-rtdb/index.js | 22 +++++++ .../tests/emulator/app-no-rtdb/package.json | 6 ++ .../tests/emulator/cascade.test.ts | 11 ++++ .../tests/emulator/helpers.ts | 19 +++++++ .../tests/emulator/no-rtdb.test.ts | 57 +++++++++++++++++++ 9 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 kits/delete-user-data/tests/emulator/app-no-rtdb/.env create mode 100644 kits/delete-user-data/tests/emulator/app-no-rtdb/index.js create mode 100644 kits/delete-user-data/tests/emulator/app-no-rtdb/package.json create mode 100644 kits/delete-user-data/tests/emulator/no-rtdb.test.ts diff --git a/kits/.gitignore b/kits/.gitignore index 4d9946868..2afaeeff7 100644 --- a/kits/.gitignore +++ b/kits/.gitignore @@ -10,7 +10,7 @@ dist/ .env .env.* # Emulator test apps commit their params: demo values, never secrets. -!*/tests/emulator/app/.env +!*/tests/emulator/*/.env # Test / coverage coverage/ diff --git a/kits/delete-user-data/firebase.json b/kits/delete-user-data/firebase.json index 6b8f0be4b..fde19fc6b 100644 --- a/kits/delete-user-data/firebase.json +++ b/kits/delete-user-data/firebase.json @@ -3,6 +3,10 @@ { "source": "tests/emulator/app", "codebase": "delete-user-data" + }, + { + "source": "tests/emulator/app-no-rtdb", + "codebase": "delete-user-data-no-rtdb" } ], "emulators": { diff --git a/kits/delete-user-data/src/index.ts b/kits/delete-user-data/src/index.ts index bd6012502..a0da72453 100644 --- a/kits/delete-user-data/src/index.ts +++ b/kits/delete-user-data/src/index.ts @@ -74,7 +74,11 @@ function getContext(): HandlerContext { ctx = { firestore: getFirestore(resolved.firestoreDatabaseId), storage: admin.storage(), - database: admin.database(), + // Resolved on first use. Without a configured RTDB instance there is no + // databaseURL to initialize the app with, and admin.database() throws. + get database() { + return admin.database(); + }, pubsub: new PubSub({ projectId: resolved.projectId }), config: resolved, }; diff --git a/kits/delete-user-data/tests/emulator/app-no-rtdb/.env b/kits/delete-user-data/tests/emulator/app-no-rtdb/.env new file mode 100644 index 000000000..694ec679b --- /dev/null +++ b/kits/delete-user-data/tests/emulator/app-no-rtdb/.env @@ -0,0 +1,15 @@ +INSTANCE_ID=nortdb +FIRESTORE_PATHS= +FIRESTORE_DATABASE_ID=(default) +FIRESTORE_DELETE_MODE=recursive +SELECTED_DATABASE_INSTANCE= +SELECTED_DATABASE_LOCATION=us-central1 +RTDB_PATHS= +CLOUD_STORAGE_BUCKET=demo-test.appspot.com +STORAGE_PATHS= +ENABLE_AUTO_DISCOVERY=true +AUTO_DISCOVERY_SEARCH_DEPTH=3 +AUTO_DISCOVERY_SEARCH_FIELDS=id,uid,userId +SEARCH_FUNCTION= +DISCOVERY_TOPIC_NAME=kit-nortdb-discovery +DELETION_TOPIC_NAME=kit-nortdb-deletion diff --git a/kits/delete-user-data/tests/emulator/app-no-rtdb/index.js b/kits/delete-user-data/tests/emulator/app-no-rtdb/index.js new file mode 100644 index 000000000..308f462be --- /dev/null +++ b/kits/delete-user-data/tests/emulator/app-no-rtdb/index.js @@ -0,0 +1,22 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Exported under a distinct name so this codebase does not shadow the +// handleSearch registered by tests/emulator/app, and without clearData so +// only one auth onDelete handler exists. +const kit = require("../../../lib/index.js"); + +exports.handleSearchNoRtdb = kit.handleSearch; diff --git a/kits/delete-user-data/tests/emulator/app-no-rtdb/package.json b/kits/delete-user-data/tests/emulator/app-no-rtdb/package.json new file mode 100644 index 000000000..bea91ddbb --- /dev/null +++ b/kits/delete-user-data/tests/emulator/app-no-rtdb/package.json @@ -0,0 +1,6 @@ +{ + "name": "delete-user-data-emulator-app-no-rtdb", + "private": true, + "main": "index.js", + "engines": { "node": "24" } +} diff --git a/kits/delete-user-data/tests/emulator/cascade.test.ts b/kits/delete-user-data/tests/emulator/cascade.test.ts index 63080b390..3805f4599 100644 --- a/kits/delete-user-data/tests/emulator/cascade.test.ts +++ b/kits/delete-user-data/tests/emulator/cascade.test.ts @@ -27,6 +27,7 @@ import { search } from "../../src/search"; import { collectionEmpty, createUser, + database, documentGone, initialize, publisherContext, @@ -191,4 +192,14 @@ describe("account deletion", () => { expect(await waitFor(documentGone(doc))).toBe(true); }); + + test("clears the configured RTDB path when a user is deleted", async () => { + const user = await createUser(auth); + const ref = database().ref(`user-data/${user.uid}`); + await ref.set({ email: user.email }); + + await auth.deleteUser(user.uid); + + expect(await waitFor(async () => !(await ref.get()).exists())).toBe(true); + }); }); diff --git a/kits/delete-user-data/tests/emulator/helpers.ts b/kits/delete-user-data/tests/emulator/helpers.ts index ee386d05f..31ad78ed3 100644 --- a/kits/delete-user-data/tests/emulator/helpers.ts +++ b/kits/delete-user-data/tests/emulator/helpers.ts @@ -21,6 +21,7 @@ import type { DocumentReference, Firestore } from "firebase-admin/firestore"; process.env.FIRESTORE_EMULATOR_HOST = "127.0.0.1:8080"; process.env.FIREBASE_AUTH_EMULATOR_HOST = "127.0.0.1:9099"; process.env.PUBSUB_EMULATOR_HOST = "127.0.0.1:8085"; +process.env.FIREBASE_DATABASE_EMULATOR_HOST = "127.0.0.1:9000"; process.env.GOOGLE_CLOUD_PROJECT = "demo-test"; export const PROJECT_ID = "demo-test"; @@ -74,3 +75,21 @@ export const documentGone = (ref: DocumentReference) => async () => export const collectionEmpty = (db: Firestore, path: string) => async (): Promise => (await db.collection(path).get()).empty; + +/** + * Separate app: the default one has no databaseURL, which is the whole point + * of the no-RTDB case the emulator suite also covers. + */ +export function database(): admin.database.Database { + const existing = admin.apps.find((app) => app?.name === "rtdb"); + const app = + existing ?? + admin.initializeApp( + { + projectId: PROJECT_ID, + databaseURL: `https://${PROJECT_ID}.firebaseio.com`, + }, + "rtdb" + ); + return (app as admin.app.App).database(); +} diff --git a/kits/delete-user-data/tests/emulator/no-rtdb.test.ts b/kits/delete-user-data/tests/emulator/no-rtdb.test.ts new file mode 100644 index 000000000..1bff07e57 --- /dev/null +++ b/kits/delete-user-data/tests/emulator/no-rtdb.test.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Firestore } from "firebase-admin/firestore"; +import { beforeAll, describe, expect, test } from "vitest"; + +import { resolveDeleteUserDataConfig } from "../../src/export-config"; +import { publishSearch } from "../../src/runBatchPubSubDeletions"; +import { + collectionEmpty, + initialize, + publisherContext, + randomId, + waitFor, +} from "./helpers"; + +// Matches tests/emulator/app-no-rtdb/.env: no Realtime Database instance, the +// default for anyone deleting only Firestore data. +const config = resolveDeleteUserDataConfig({ + instanceId: "nortdb", + projectId: "demo-test", + discoveryTopicName: "kit-nortdb-discovery", + deletionTopicName: "kit-nortdb-deletion", +}); + +let db: Firestore; +let ctx: ReturnType; + +beforeAll(() => { + ({ db } = initialize()); + ctx = publisherContext(config); +}); + +describe("with no Realtime Database instance configured", () => { + test("still runs discovery instead of dying on startup", async () => { + const uid = randomId(); + const collection = db.collection(randomId()).doc("parent").collection(uid); + await collection.add({ foo: "bar" }); + + await publishSearch(uid, 1, collection.path, ctx); + + expect(await waitFor(collectionEmpty(db, collection.path))).toBe(true); + }); +});