Skip to content

Commit 26e46ce

Browse files
committed
fix(delete-user-data): recursive delete searched docs if enabled (#2073)
* fix(delete-user-data): recursive delete searched docs if enabled * chore(delete-user-data): move test to __tests__
1 parent 3c705ed commit 26e46ce

File tree

8 files changed

+224
-23
lines changed

8 files changed

+224
-23
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { handleDeletion } from "../src";
2+
import * as admin from "firebase-admin"; // Import Firebase admin to mock Firestore
3+
import firebaseFunctionsTest from "firebase-functions-test";
4+
import { search } from "../src/search";
5+
import { Message } from "@google-cloud/pubsub";
6+
7+
const testEnv = firebaseFunctionsTest();
8+
const { wrap } = testEnv;
9+
10+
jest.mock("../src/config", () => ({
11+
location: "us-central1",
12+
firestorePaths: "test",
13+
firestoreDeleteMode: "recursive",
14+
rtdbPaths: undefined,
15+
storagePaths: undefined,
16+
enableSearch: true,
17+
storageBucketDefault: "test",
18+
selectedDatabaseInstance: "test",
19+
selectedDatabaseLocation: "us-central1",
20+
searchFields: "uid",
21+
searchFunction: undefined,
22+
discoveryTopic: "ext-test-discovery",
23+
deletionTopic: "ext-test-deletion",
24+
searchDepth: 3,
25+
}));
26+
const wrapped = wrap(handleDeletion);
27+
const wrappedHandleDelete = ({
28+
uid,
29+
paths,
30+
}: {
31+
uid: string;
32+
paths: string[];
33+
}) => {
34+
//@ts-ignore
35+
return wrapped({
36+
data: Buffer.from(JSON.stringify({ uid, paths })).toString("base64"),
37+
});
38+
};
39+
40+
const db = admin.firestore();
41+
42+
describe("handleDelete", () => {
43+
test("should delete valid paths correctly", async () => {
44+
const paths = ["valid/path1", "valid/path2"];
45+
46+
for (const path of paths) {
47+
await db.doc(path).set({ uid: "testUid" });
48+
}
49+
await wrappedHandleDelete({ uid: "testUid", paths });
50+
51+
const checkExists = await Promise.all(
52+
paths.map(async (path) => {
53+
const doc = await db.doc(path).get();
54+
return doc.exists;
55+
})
56+
);
57+
for (const exists of checkExists) {
58+
expect(exists).toBe(false);
59+
}
60+
});
61+
62+
test("should delete subcollections of matching docs", async () => {
63+
const paths = ["valid/path1", "valid/path2"];
64+
65+
for (const path of paths) {
66+
await db.doc(path).set({ uid: "testUid" });
67+
await db.doc(`${path}/subcollection/doc`).set({ foo: "bar" });
68+
}
69+
70+
await wrappedHandleDelete({ uid: "testUid", paths });
71+
72+
const checkExists = await Promise.all(
73+
paths.map(async (path) => {
74+
const doc = await db.doc(path).get();
75+
return doc.exists;
76+
})
77+
);
78+
79+
for (const exists of checkExists) {
80+
expect(exists).toBe(false);
81+
}
82+
83+
const subcollectionDocs = await Promise.all(
84+
paths.map(async (path) => {
85+
const snapshot = await db.doc(`${path}/subcollection/doc`).get();
86+
return snapshot.exists;
87+
})
88+
);
89+
90+
for (const exists of subcollectionDocs) {
91+
expect(exists).toBe(false);
92+
}
93+
});
94+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Import your function and any necessary Firebase modules
2+
import { recursiveDelete } from "../src/recursiveDelete"; // Update with your actual file path
3+
import * as admin from "firebase-admin";
4+
5+
const bulkWriterMock = () => ({
6+
onWriteError: jest.fn(),
7+
close: jest.fn(() => Promise.resolve()),
8+
});
9+
// Mock admin and firestore
10+
11+
admin.initializeApp();
12+
13+
describe("recursiveDelete", () => {
14+
// Common setup
15+
const db = admin.firestore();
16+
17+
test("successfully deletes a document reference", async () => {
18+
const ref = "documents/doc1";
19+
db.doc(ref).create({
20+
foo: "bar",
21+
});
22+
23+
await recursiveDelete(ref);
24+
25+
const doc = db.doc(ref);
26+
await doc.get().then((doc) => {
27+
expect(doc.exists).toBe(false);
28+
});
29+
});
30+
31+
test("successfully deletes a collection reference", async () => {
32+
const ref = "documents/doc1/collection1";
33+
db.collection(ref).add({
34+
foo: "bar",
35+
});
36+
37+
await recursiveDelete(ref);
38+
39+
const collection = db.collection(ref);
40+
await collection.get().then((collection) => {
41+
expect(collection.docs.length).toBe(0);
42+
});
43+
});
44+
45+
test("successfully deletes a document with a subcollection", async () => {
46+
const parentRef = "documents/doc1";
47+
const ref = "documents/doc1/collection1/doc2/collection2";
48+
db.collection(ref).add({
49+
foo: "bar",
50+
});
51+
52+
await recursiveDelete(parentRef);
53+
54+
const collection = db.collection(ref);
55+
await collection.get().then((collection) => {
56+
expect(collection.docs.length).toBe(0);
57+
});
58+
});
59+
});

delete-user-data/functions/package-lock.json

+43
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

delete-user-data/functions/package.json

+7-5
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
"compile": "tsc",
1111
"local:emulator": "cd ../../_emulator && firebase emulators:start -P demo-test",
1212
"test": "cd ../../_emulator && firebase emulators:exec jest -P demo-test",
13-
"test:local": "concurrently \"npm run local:emulator\" \"jest\"",
13+
"test:local": "cd ../../_emulator && firebase emulators:exec \"CI_TEST=true jest --detectOpenHandles --verbose --forceExit --testMatch **/delete-user-data/**/*.test.ts\"",
1414
"test:watch": "concurrently \"npm run local:emulator\" \"jest --watch\"",
15-
"generate-readme": "firebase ext:info .. --markdown > ../README.md"
15+
"generate-readme": "firebase ext:info .. --markdown > ../README.md",
16+
"test:emulator-running": "jest"
1617
},
1718
"author": "Lauren Long <[email protected]>",
1819
"license": "Apache-2.0",
@@ -33,13 +34,14 @@
3334
},
3435
"private": true,
3536
"devDependencies": {
37+
"@types/jest": "29.5.0",
3638
"@types/lodash.chunk": "^4.2.7",
3739
"@types/node-fetch": "^2.6.2",
3840
"concurrency": "^0.1.4",
3941
"dotenv": "^16.0.2",
40-
"wait-port": "^0.2.9",
41-
"@types/jest": "29.5.0",
42+
"firebase-functions-test": "^3.2.0",
4243
"jest": "29.5.0",
43-
"ts-jest": "29.1.2"
44+
"ts-jest": "29.1.2",
45+
"wait-port": "^0.2.9"
4446
}
4547
}

delete-user-data/functions/src/index.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,11 @@ export const handleDeletion = functions.pubsub
8181
invalidPaths.push(path);
8282
continue;
8383
}
84-
85-
batch.delete(docRef);
84+
if (config.firestoreDeleteMode === "recursive") {
85+
await recursiveDelete(path);
86+
} else {
87+
batch.delete(docRef);
88+
}
8689
}
8790

8891
batchArray.push(batch);
@@ -155,7 +158,7 @@ export const handleSearch = functions.pubsub
155158
if (reference.id === uid) {
156159
pathsToDelete.push(reference.path);
157160
}
158-
// If the user has search fields, all the document to the list of documents to search.
161+
// If the user has search fields, add the document to the list of documents to search.
159162
else if (config.searchFields) {
160163
documentReferencesToSearch.push(reference);
161164
}

delete-user-data/functions/src/recursiveDelete.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as admin from "firebase-admin";
22

33
const MAX_RETRY_ATTEMPTS = 3;
44

5-
export const recursiveDelete = async (ref: string) => {
5+
export const recursiveDelete = async (path: string) => {
66
const db = admin.firestore();
77
// Recursively delete a reference and log the references of failures.
88
const bulkWriter = db.bulkWriter();
@@ -16,9 +16,9 @@ export const recursiveDelete = async (ref: string) => {
1616
}
1717
});
1818

19-
const isDocument = ref.split("/").length % 2 === 0;
19+
const isDocument = path.split("/").length % 2 === 0;
2020

21-
const reference = isDocument ? db.doc(ref) : db.collection(ref);
21+
const reference = isDocument ? db.doc(path) : db.collection(path);
2222

2323
await db.recursiveDelete(reference, bulkWriter);
2424
};

package-lock.json

+7-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@
2626
"url": ""
2727
},
2828
"devDependencies": {
29+
"@types/jest": "29.5.0",
2930
"codecov": "^3.8.1",
30-
"husky": "^7.0.4",
31-
"lint-staged": "^12.4.0",
3231
"concurrently": "^7.2.1",
33-
"@types/jest": "29.5.0",
34-
"jest": "29.5.0",
35-
"ts-jest": "29.1.2",
32+
"husky": "^7.0.4",
33+
"jest": "^29.7.0",
3634
"lerna": "^3.4.3",
35+
"lint-staged": "^12.4.0",
3736
"prettier": "2.7.1",
37+
"ts-jest": "29.1.2",
3838
"typescript": "^4.8.4"
3939
},
4040
"lint-staged": {

0 commit comments

Comments
 (0)