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); + }); +});