Skip to content

Commit e647d87

Browse files
authored
impr: error handling on user deletion (@fehmer) (monkeytypegame#6363)
!nuf
1 parent a94a6db commit e647d87

File tree

2 files changed

+166
-11
lines changed

2 files changed

+166
-11
lines changed

backend/__tests__/api/controllers/user.spec.ts

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
import { randomUUID } from "node:crypto";
3030
import _ from "lodash";
3131
import { MonkeyMail, UserStreak } from "@monkeytype/contracts/schemas/users";
32-
import { isFirebaseError } from "../../../src/utils/error";
32+
import MonkeyError, { isFirebaseError } from "../../../src/utils/error";
3333
import { LeaderboardEntry } from "@monkeytype/contracts/schemas/leaderboards";
3434
import * as WeeklyXpLeaderboard from "../../../src/services/weekly-xp-leaderboard";
3535

@@ -704,6 +704,141 @@ describe("user controller test", () => {
704704
(await configuration).leaderboards.weeklyXp
705705
);
706706
});
707+
708+
it("should not fail if userInfo cannot be found", async () => {
709+
//GIVEN
710+
getUserMock.mockRejectedValue(new MonkeyError(404, "user not found"));
711+
712+
//WHEN
713+
await mockApp
714+
.delete("/users/")
715+
.set("Authorization", `Bearer ${uid}`)
716+
.expect(200);
717+
718+
//THEN
719+
expect(blocklistAddMock).not.toHaveBeenCalled();
720+
721+
expect(deleteUserMock).toHaveBeenCalledWith(uid);
722+
expect(firebaseDeleteUserMock).toHaveBeenCalledWith(uid);
723+
expect(deleteAllApeKeysMock).toHaveBeenCalledWith(uid);
724+
expect(deleteAllPresetsMock).toHaveBeenCalledWith(uid);
725+
expect(deleteConfigMock).toHaveBeenCalledWith(uid);
726+
expect(deleteAllResultMock).toHaveBeenCalledWith(uid);
727+
expect(purgeUserFromDailyLeaderboardsMock).toHaveBeenCalledWith(
728+
uid,
729+
(await configuration).dailyLeaderboards
730+
);
731+
expect(purgeUserFromXpLeaderboardsMock).toHaveBeenCalledWith(
732+
uid,
733+
(await configuration).leaderboards.weeklyXp
734+
);
735+
});
736+
737+
it("should fail for unknown error from UserDal", async () => {
738+
//GIVEN
739+
getUserMock.mockRejectedValue(new Error("oops"));
740+
741+
//WHEN
742+
await mockApp
743+
.delete("/users/")
744+
.set("Authorization", `Bearer ${uid}`)
745+
.expect(500);
746+
747+
//THEN
748+
expect(blocklistAddMock).not.toHaveBeenCalled();
749+
expect(deleteUserMock).not.toHaveBeenCalledWith(uid);
750+
expect(firebaseDeleteUserMock).not.toHaveBeenCalledWith(uid);
751+
expect(deleteAllApeKeysMock).not.toHaveBeenCalledWith(uid);
752+
expect(deleteAllPresetsMock).not.toHaveBeenCalledWith(uid);
753+
expect(deleteConfigMock).not.toHaveBeenCalledWith(uid);
754+
expect(deleteAllResultMock).not.toHaveBeenCalledWith(uid);
755+
expect(purgeUserFromDailyLeaderboardsMock).not.toHaveBeenCalledWith(
756+
uid,
757+
(await configuration).dailyLeaderboards
758+
);
759+
expect(purgeUserFromXpLeaderboardsMock).not.toHaveBeenCalledWith(
760+
uid,
761+
(await configuration).leaderboards.weeklyXp
762+
);
763+
});
764+
it("should not fail if firebase user cannot be found", async () => {
765+
//GIVEN
766+
const user = {
767+
uid,
768+
name: "name",
769+
email: "email",
770+
discordId: "discordId",
771+
} as Partial<UserDal.DBUser> as UserDal.DBUser;
772+
getUserMock.mockResolvedValue(user);
773+
firebaseDeleteUserMock.mockRejectedValue({
774+
code: "user-not-found",
775+
codePrefix: "auth",
776+
errorInfo: { code: "auth/user-not-found", message: "user not found" },
777+
});
778+
779+
//WHEN
780+
await mockApp
781+
.delete("/users/")
782+
.set("Authorization", `Bearer ${uid}`)
783+
.expect(200);
784+
785+
//THEN
786+
expect(blocklistAddMock).not.toHaveBeenCalled();
787+
788+
expect(deleteUserMock).toHaveBeenCalledWith(uid);
789+
expect(firebaseDeleteUserMock).toHaveBeenCalledWith(uid);
790+
expect(deleteAllApeKeysMock).toHaveBeenCalledWith(uid);
791+
expect(deleteAllPresetsMock).toHaveBeenCalledWith(uid);
792+
expect(deleteConfigMock).toHaveBeenCalledWith(uid);
793+
expect(deleteAllResultMock).toHaveBeenCalledWith(uid);
794+
expect(purgeUserFromDailyLeaderboardsMock).toHaveBeenCalledWith(
795+
uid,
796+
(await configuration).dailyLeaderboards
797+
);
798+
expect(purgeUserFromXpLeaderboardsMock).toHaveBeenCalledWith(
799+
uid,
800+
(await configuration).leaderboards.weeklyXp
801+
);
802+
});
803+
804+
it("should fail for unknown error from firebase", async () => {
805+
//GIVEN
806+
const user = {
807+
uid,
808+
name: "name",
809+
email: "email",
810+
discordId: "discordId",
811+
} as Partial<UserDal.DBUser> as UserDal.DBUser;
812+
getUserMock.mockResolvedValue(user);
813+
firebaseDeleteUserMock.mockRejectedValue({
814+
code: "unknown",
815+
codePrefix: "auth",
816+
errorInfo: { code: "auth/unknown", message: "unknown" },
817+
});
818+
819+
//WHEN
820+
await mockApp
821+
.delete("/users/")
822+
.set("Authorization", `Bearer ${uid}`)
823+
.expect(500);
824+
825+
//THEN
826+
expect(blocklistAddMock).not.toHaveBeenCalled();
827+
expect(deleteUserMock).toHaveBeenCalledWith(uid);
828+
expect(firebaseDeleteUserMock).toHaveBeenCalledWith(uid);
829+
expect(deleteAllApeKeysMock).toHaveBeenCalledWith(uid);
830+
expect(deleteAllPresetsMock).toHaveBeenCalledWith(uid);
831+
expect(deleteConfigMock).toHaveBeenCalledWith(uid);
832+
expect(deleteAllResultMock).toHaveBeenCalledWith(uid);
833+
expect(purgeUserFromDailyLeaderboardsMock).toHaveBeenCalledWith(
834+
uid,
835+
(await configuration).dailyLeaderboards
836+
);
837+
expect(purgeUserFromXpLeaderboardsMock).toHaveBeenCalledWith(
838+
uid,
839+
(await configuration).leaderboards.weeklyXp
840+
);
841+
});
707842
});
708843
describe("resetUser", () => {
709844
const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser");

backend/src/api/controllers/user.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -259,14 +259,26 @@ export async function sendForgotPasswordEmail(
259259
export async function deleteUser(req: MonkeyRequest): Promise<MonkeyResponse> {
260260
const { uid } = req.ctx.decodedToken;
261261

262-
const userInfo = await UserDAL.getPartialUser(uid, "delete user", [
263-
"banned",
264-
"name",
265-
"email",
266-
"discordId",
267-
]);
262+
let userInfo:
263+
| Pick<UserDAL.DBUser, "banned" | "name" | "email" | "discordId">
264+
| undefined;
268265

269-
if (userInfo.banned === true) {
266+
try {
267+
userInfo = await UserDAL.getPartialUser(uid, "delete user", [
268+
"banned",
269+
"name",
270+
"email",
271+
"discordId",
272+
]);
273+
} catch (e) {
274+
if (e instanceof MonkeyError && e.status === 404) {
275+
//userinfo was already deleted. We ignore this and still try to remove the other data
276+
} else {
277+
throw e;
278+
}
279+
}
280+
281+
if (userInfo?.banned === true) {
270282
await BlocklistDal.add(userInfo);
271283
}
272284

@@ -288,12 +300,20 @@ export async function deleteUser(req: MonkeyRequest): Promise<MonkeyResponse> {
288300
),
289301
]);
290302

291-
//delete user from
292-
await AuthUtil.deleteUser(uid);
303+
try {
304+
//delete user from firebase
305+
await AuthUtil.deleteUser(uid);
306+
} catch (e) {
307+
if (isFirebaseError(e) && e.errorInfo.code === "auth/user-not-found") {
308+
//user was already deleted, ok to ignore
309+
} else {
310+
throw e;
311+
}
312+
}
293313

294314
void addImportantLog(
295315
"user_deleted",
296-
`${userInfo.email} ${userInfo.name}`,
316+
`${userInfo?.email} ${userInfo?.name}`,
297317
uid
298318
);
299319

0 commit comments

Comments
 (0)