aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChristian Fehmer <[email protected]>2024-06-24 13:55:13 +0200
committerGitHub <[email protected]>2024-06-24 13:55:13 +0200
commit442153724a688f2627d9bd205e8289049e6e9855 (patch)
tree0227eb5d5759790a05a3ae09da64a6eb980a711e
parentbfc9500d324bdc8da58ec8fe1f6cd8158aa79833 (diff)
downloadmonkeytype-442153724a688f2627d9bd205e8289049e6e9855.tar.gz
monkeytype-442153724a688f2627d9bd205e8289049e6e9855.zip
feat: add test activity and streak into to the apekey endpoints (@fehmer) (#5513)
* feat: add test activity and streak into to the apekey endpoints (@fehmer) * add public conract * review comments
-rw-r--r--backend/__tests__/api/controllers/user.spec.ts100
-rw-r--r--backend/src/api/controllers/user.ts24
-rw-r--r--backend/src/api/routes/users.ts17
-rw-r--r--backend/src/documentation/public-swagger.json64
-rw-r--r--backend/src/middlewares/rate-limit.ts14
5 files changed, 202 insertions, 17 deletions
diff --git a/backend/__tests__/api/controllers/user.spec.ts b/backend/__tests__/api/controllers/user.spec.ts
index 30174e413..4086ae5b7 100644
--- a/backend/__tests__/api/controllers/user.spec.ts
+++ b/backend/__tests__/api/controllers/user.spec.ts
@@ -1,7 +1,7 @@
import request from "supertest";
import app from "../../../src/app";
import * as Configuration from "../../../src/init/configuration";
-import { getCurrentTestActivity } from "../../../src/api/controllers/user";
+import { generateCurrentTestActivity } from "../../../src/api/controllers/user";
import * as UserDal from "../../../src/dal/user";
import _ from "lodash";
import { DecodedIdToken } from "firebase-admin/lib/auth/token-verifier";
@@ -203,7 +203,7 @@ describe("user controller test", () => {
//given
getUserMock.mockResolvedValue({
testActivity: { "2023": [1, 2, 3], "2024": [4, 5, 6] },
- } as unknown as MonkeyTypes.DBUser);
+ } as Partial<MonkeyTypes.DBUser> as MonkeyTypes.DBUser);
//when
await mockApp
@@ -216,7 +216,7 @@ describe("user controller test", () => {
//given
getUserMock.mockResolvedValue({
testActivity: { "2023": [1, 2, 3], "2024": [4, 5, 6] },
- } as unknown as MonkeyTypes.DBUser);
+ } as Partial<MonkeyTypes.DBUser> as MonkeyTypes.DBUser);
vi.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(true);
await enablePremiumFeatures(true);
@@ -234,12 +234,12 @@ describe("user controller test", () => {
});
});
- describe("getCurrentTestActivity", () => {
+ describe("generateCurrentTestActivity", () => {
beforeAll(() => {
vi.useFakeTimers().setSystemTime(1712102400000);
});
it("without any data", () => {
- expect(getCurrentTestActivity(undefined)).toBeUndefined();
+ expect(generateCurrentTestActivity(undefined)).toBeUndefined();
});
it("with current year only", () => {
//given
@@ -248,7 +248,7 @@ describe("user controller test", () => {
};
//when
- const testActivity = getCurrentTestActivity(data);
+ const testActivity = generateCurrentTestActivity(data);
//then
expect(testActivity?.lastDay).toEqual(1712102400000);
@@ -268,7 +268,7 @@ describe("user controller test", () => {
};
//when
- const testActivity = getCurrentTestActivity(data);
+ const testActivity = generateCurrentTestActivity(data);
//then
expect(testActivity?.lastDay).toEqual(1712102400000);
@@ -288,7 +288,7 @@ describe("user controller test", () => {
};
//when
- const testActivity = getCurrentTestActivity(data);
+ const testActivity = generateCurrentTestActivity(data);
//then
expect(testActivity?.lastDay).toEqual(1712102400000);
@@ -326,7 +326,7 @@ describe("user controller test", () => {
name: "name",
email: "email",
discordId: "discordId",
- } as unknown as MonkeyTypes.DBUser;
+ } as Partial<MonkeyTypes.DBUser> as MonkeyTypes.DBUser;
getUserMock.mockResolvedValue(user);
//WHEN
@@ -352,7 +352,7 @@ describe("user controller test", () => {
name: "name",
email: "email",
discordId: "",
- } as unknown as MonkeyTypes.DBUser;
+ } as Partial<MonkeyTypes.DBUser> as MonkeyTypes.DBUser;
getUserMock.mockResolvedValue(user);
//WHEN
@@ -378,7 +378,7 @@ describe("user controller test", () => {
email: "email",
discordId: "discordId",
banned: true,
- } as unknown as MonkeyTypes.DBUser;
+ } as Partial<MonkeyTypes.DBUser> as MonkeyTypes.DBUser;
getUserMock.mockResolvedValue(user);
//WHEN
@@ -406,7 +406,7 @@ describe("user controller test", () => {
email: "email",
discordId: "",
banned: true,
- } as unknown as MonkeyTypes.DBUser;
+ } as Partial<MonkeyTypes.DBUser> as MonkeyTypes.DBUser;
getUserMock.mockResolvedValue(user);
//WHEN
@@ -475,7 +475,7 @@ describe("user controller test", () => {
email: "email",
discordId: "discordId",
banned: true,
- } as unknown as MonkeyTypes.DBUser;
+ } as Partial<MonkeyTypes.DBUser> as MonkeyTypes.DBUser;
await getUserMock.mockResolvedValue(user);
//WHEN
@@ -509,7 +509,7 @@ describe("user controller test", () => {
name: "name",
email: "email",
discordId: "discordId",
- } as unknown as MonkeyTypes.DBUser;
+ } as Partial<MonkeyTypes.DBUser> as MonkeyTypes.DBUser;
getUserMock.mockResolvedValue(user);
//WHEN
@@ -574,7 +574,7 @@ describe("user controller test", () => {
uid,
name: "name",
email: "email",
- } as unknown as MonkeyTypes.DBUser;
+ } as Partial<MonkeyTypes.DBUser> as MonkeyTypes.DBUser;
getUserMock.mockResolvedValue(user);
blocklistContainsMock.mockResolvedValue(true);
@@ -600,6 +600,76 @@ describe("user controller test", () => {
});
});
});
+ describe("getCurrentTestActivity", () => {
+ const getUserMock = vi.spyOn(UserDal, "getUser");
+
+ afterEach(() => {
+ getUserMock.mockReset();
+ });
+ it("gets", async () => {
+ //GIVEN
+ vi.useFakeTimers().setSystemTime(1712102400000);
+ const user = {
+ uid: mockDecodedToken.uid,
+ testActivity: {
+ "2024": fillYearWithDay(94),
+ },
+ } as Partial<MonkeyTypes.DBUser> as MonkeyTypes.DBUser;
+ getUserMock.mockResolvedValue(user);
+
+ //WHEN
+ const result = await mockApp
+ .get("/users/currentTestActivity")
+ .set("Authorization", "Bearer 123456789")
+ .send()
+ .expect(200);
+
+ //THEN
+ expect(result.body.data.lastDay).toEqual(1712102400000);
+ const testsByDays = result.body.data.testsByDays;
+ expect(testsByDays).toHaveLength(372);
+ expect(testsByDays[6]).toEqual(null); //2023-04-04
+ expect(testsByDays[277]).toEqual(null); //2023-12-31
+ expect(testsByDays[278]).toEqual(1); //2024-01-01
+ expect(testsByDays[371]).toEqual(94); //2024-01
+ });
+ });
+ describe("getStreak", () => {
+ const getUserMock = vi.spyOn(UserDal, "getUser");
+
+ afterEach(() => {
+ getUserMock.mockReset();
+ });
+ it("gets", async () => {
+ //GIVEN
+ const user = {
+ uid: mockDecodedToken.uid,
+ streak: {
+ lastResultTimestamp: 1712102400000,
+ length: 42,
+ maxLength: 1024,
+ hourOffset: 2,
+ },
+ } as Partial<MonkeyTypes.DBUser> as MonkeyTypes.DBUser;
+ getUserMock.mockResolvedValue(user);
+
+ //WHEN
+ const result = await mockApp
+ .get("/users/streak")
+ .set("Authorization", "Bearer 123456789")
+ .send()
+ .expect(200);
+
+ //THEN
+ const streak: SharedTypes.UserStreak = result.body.data;
+ expect(streak).toEqual({
+ lastResultTimestamp: 1712102400000,
+ length: 42,
+ maxLength: 1024,
+ hourOffset: 2,
+ });
+ });
+ });
});
function fillYearWithDay(days: number): number[] {
diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts
index 558cd13f3..304cc8008 100644
--- a/backend/src/api/controllers/user.ts
+++ b/backend/src/api/controllers/user.ts
@@ -449,7 +449,7 @@ export async function getUser(
const isPremium = await UserDAL.checkIfUserIsPremium(uid, userInfo);
const allTimeLbs = await getAllTimeLbs(uid);
- const testActivity = getCurrentTestActivity(userInfo.testActivity);
+ const testActivity = generateCurrentTestActivity(userInfo.testActivity);
const userData = {
...getRelevantUserInfo(userInfo),
@@ -995,7 +995,7 @@ async function getAllTimeLbs(uid: string): Promise<SharedTypes.AllTimeLbs> {
};
}
-export function getCurrentTestActivity(
+export function generateCurrentTestActivity(
testActivity: SharedTypes.CountByYearAndDay | undefined
): SharedTypes.TestActivity | undefined {
const thisYear = Dates.startOfYear(new UTCDateMini());
@@ -1057,3 +1057,23 @@ async function firebaseDeleteUserIgnoreError(uid: string): Promise<void> {
//ignore
}
}
+
+export async function getCurrentTestActivity(
+ req: MonkeyTypes.Request
+): Promise<MonkeyResponse> {
+ const { uid } = req.ctx.decodedToken;
+
+ const user = await UserDAL.getUser(uid, "current test activity");
+ const data = generateCurrentTestActivity(user.testActivity);
+ return new MonkeyResponse("Current test activity data retrieved", data);
+}
+
+export async function getStreak(
+ req: MonkeyTypes.Request
+): Promise<MonkeyResponse> {
+ const { uid } = req.ctx.decodedToken;
+
+ const user = await UserDAL.getUser(uid, "streak");
+
+ return new MonkeyResponse("Streak data retrieved", user.streak);
+}
diff --git a/backend/src/api/routes/users.ts b/backend/src/api/routes/users.ts
index 5faaacf72..1ccca515e 100644
--- a/backend/src/api/routes/users.ts
+++ b/backend/src/api/routes/users.ts
@@ -681,4 +681,21 @@ router.get(
asyncHandler(UserController.getTestActivity)
);
+router.get(
+ "/currentTestActivity",
+ authenticateRequest({
+ acceptApeKeys: true,
+ }),
+ withApeRateLimiter(RateLimit.userCurrentTestActivity),
+ asyncHandler(UserController.getCurrentTestActivity)
+);
+
+router.get(
+ "/streak",
+ authenticateRequest({
+ acceptApeKeys: true,
+ }),
+ withApeRateLimiter(RateLimit.userStreak),
+ asyncHandler(UserController.getStreak)
+);
export default router;
diff --git a/backend/src/documentation/public-swagger.json b/backend/src/documentation/public-swagger.json
index 7fdd3d195..aadc66ea5 100644
--- a/backend/src/documentation/public-swagger.json
+++ b/backend/src/documentation/public-swagger.json
@@ -120,6 +120,34 @@
}
}
},
+ "/users/currentTestActivity": {
+ "get": {
+ "tags": ["users"],
+ "summary": "Gets a user's test activity data for the last ~52 weeks",
+ "responses": {
+ "200": {
+ "description": "",
+ "schema": {
+ "$ref": "#/definitions/CurrentTestActivity"
+ }
+ }
+ }
+ }
+ },
+ "/users/streak": {
+ "get": {
+ "tags": ["users"],
+ "summary": "Gets a user's streak",
+ "responses": {
+ "200": {
+ "description": "",
+ "schema": {
+ "$ref": "#/definitions/UserStreak"
+ }
+ }
+ }
+ }
+ },
"/results": {
"get": {
"tags": ["results"],
@@ -786,6 +814,42 @@
}
}
}
+ },
+ "CurrentTestActivity": {
+ "type": "object",
+ "properties": {
+ "testByDays": {
+ "type": "array",
+ "items": {
+ "type": "number",
+ "nullable": true
+ },
+ "example": [null, null, null, 1, 2, 3, null, 4],
+ "description": "Test activity by day. Last element of the array are the tests on the date specified by the `lastDay` property. All dates are in UTC."
+ },
+ "lastDay": {
+ "type": "integer",
+ "example": 1712140496000
+ }
+ }
+ },
+ "UserStreak": {
+ "type": "object",
+ "properties": {
+ "lastResultTimestamp": {
+ "type": "integer"
+ },
+ "length": {
+ "type": "integer"
+ },
+ "maxLength": {
+ "type": "integer"
+ },
+ "hourOffset": {
+ "type": "integer",
+ "nullable": true
+ }
+ }
}
}
}
diff --git a/backend/src/middlewares/rate-limit.ts b/backend/src/middlewares/rate-limit.ts
index 1103d6f18..1189ca747 100644
--- a/backend/src/middlewares/rate-limit.ts
+++ b/backend/src/middlewares/rate-limit.ts
@@ -526,6 +526,20 @@ export const userTestActivity = rateLimit({
handler: customHandler,
});
+export const userCurrentTestActivity = rateLimit({
+ windowMs: ONE_HOUR_MS,
+ max: 60 * REQUEST_MULTIPLIER,
+ keyGenerator: getKeyWithUid,
+ handler: customHandler,
+});
+
+export const userStreak = rateLimit({
+ windowMs: ONE_HOUR_MS,
+ max: 60 * REQUEST_MULTIPLIER,
+ keyGenerator: getKeyWithUid,
+ handler: customHandler,
+});
+
// ApeKeys Routing
export const apeKeysGet = rateLimit({
windowMs: ONE_HOUR_MS,