impr: add function to clearstreakhouroffset

!nuf
This commit is contained in:
Miodec 2025-05-12 13:52:37 +02:00
parent 98f2b9ceca
commit 8370de1fa4
6 changed files with 154 additions and 0 deletions

View file

@ -194,6 +194,95 @@ describe("AdminController", () => {
).toBeRateLimited({ max: 1, windowMs: 5000 });
});
});
describe("clear streak hour offset", () => {
const clearStreakHourOffset = vi.spyOn(UserDal, "clearStreakHourOffset");
beforeEach(() => {
[clearStreakHourOffset].forEach((it) => it.mockReset());
});
it("should clear streak hour offset for user", async () => {
//GIVEN
const victimUid = new ObjectId().toHexString();
//WHEN
const { body } = await mockApp
.post("/admin/clearStreakHourOffset")
.send({ uid: victimUid })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body).toEqual({
message: "Streak hour offset cleared",
data: null,
});
expect(clearStreakHourOffset).toHaveBeenCalledWith(victimUid);
});
it("should fail without mandatory properties", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.post("/admin/clearStreakHourOffset")
.send({})
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ['"uid" Required'],
});
});
it("should fail with unknown properties", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.post("/admin/clearStreakHourOffset")
.send({ uid: new ObjectId().toHexString(), extra: "value" })
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
it("should fail if user is no admin", async () => {
await expectFailForNonAdmin(
mockApp
.post("/admin/clearStreakHourOffset")
.send({ uid: new ObjectId().toHexString() })
.set("Authorization", `Bearer ${uid}`)
);
});
it("should fail if admin endpoints are disabled", async () => {
//GIVEN
await expectFailForDisabledEndpoint(
mockApp
.post("/admin/clearStreakHourOffset")
.send({ uid: new ObjectId().toHexString() })
.set("Authorization", `Bearer ${uid}`)
);
});
it("should be rate limited", async () => {
//GIVEN
const victimUid = new ObjectId().toHexString();
//WHEN
await expect(
mockApp
.post("/admin/clearStreakHourOffset")
.send({ uid: victimUid })
.set("Authorization", `Bearer ${uid}`)
).toBeRateLimited({ max: 1, windowMs: 5000 });
});
});
describe("accept reports", () => {
const getReportsMock = vi.spyOn(ReportDal, "getReports");
const deleteReportsMock = vi.spyOn(ReportDal, "deleteReports");

View file

@ -2011,4 +2011,23 @@ describe("UserDal", () => {
});
});
});
describe("clearStreakHourOffset", () => {
it("should clear streak hour offset", async () => {
// given
const { uid } = await UserTestData.createUser({
//@ts-expect-error
streak: {
hourOffset: 1,
},
});
// when
await UserDAL.clearStreakHourOffset(uid);
//then
const read = await UserDAL.getUser(uid, "read");
expect(read.streak?.hourOffset).toBeUndefined();
});
});
});

View file

@ -6,6 +6,7 @@ import GeorgeQueue from "../../queues/george-queue";
import { sendForgotPasswordEmail as authSendForgotPasswordEmail } from "../../utils/auth";
import {
AcceptReportsRequest,
ClearStreakHourOffsetRequest,
RejectReportsRequest,
SendForgotPasswordEmailRequest,
ToggleBanRequest,
@ -42,6 +43,17 @@ export async function toggleBan(
});
}
export async function clearStreakHourOffset(
req: MonkeyRequest<undefined, ClearStreakHourOffsetRequest>
): Promise<MonkeyResponse> {
const { uid } = req.body;
await UserDAL.clearStreakHourOffset(uid);
void addImportantLog("admin_streak_hour_offset_cleared_by", {}, uid);
return new MonkeyResponse("Streak hour offset cleared", null);
}
export async function acceptReports(
req: MonkeyRequest<undefined, AcceptReportsRequest>
): Promise<MonkeyResponse> {

View file

@ -13,6 +13,10 @@ export default s.router(adminContract, {
toggleBan: {
handler: async (r) => callController(AdminController.toggleBan)(r),
},
clearStreakHourOffset: {
handler: async (r) =>
callController(AdminController.clearStreakHourOffset)(r),
},
acceptReports: {
handler: async (r) => callController(AdminController.acceptReports)(r),
},

View file

@ -1146,6 +1146,17 @@ export async function setBanned(uid: string, banned: boolean): Promise<void> {
}
}
export async function clearStreakHourOffset(uid: string): Promise<void> {
await getUsersCollection().updateOne(
{ uid },
{
$unset: {
"streak.hourOffset": "",
},
}
);
}
export async function checkIfUserIsPremium(
uid: string,
userInfoOverride?: Pick<DBUser, "premium">

View file

@ -15,6 +15,15 @@ export const ToggleBanRequestSchema = z
.strict();
export type ToggleBanRequest = z.infer<typeof ToggleBanRequestSchema>;
export const ClearStreakHourOffsetRequestSchema = z
.object({
uid: IdSchema,
})
.strict();
export type ClearStreakHourOffsetRequest = z.infer<
typeof ClearStreakHourOffsetRequestSchema
>;
export const ToggleBanResponseSchema = responseWithData(
z.object({
banned: z.boolean(),
@ -73,6 +82,16 @@ export const adminContract = c.router(
200: ToggleBanResponseSchema,
},
},
clearStreakHourOffset: {
summary: "clear streak hour offset",
description: "Clear the streak hour offset for a user",
method: "POST",
path: "/clearStreakHourOffset",
body: ClearStreakHourOffsetRequestSchema,
responses: {
200: MonkeyResponseSchema,
},
},
acceptReports: {
summary: "accept reports",
description: "Accept one or many reports",