diff options
author | Christian Fehmer <[email protected]> | 2024-06-24 13:55:13 +0200 |
---|---|---|
committer | GitHub <[email protected]> | 2024-06-24 13:55:13 +0200 |
commit | 442153724a688f2627d9bd205e8289049e6e9855 (patch) | |
tree | 0227eb5d5759790a05a3ae09da64a6eb980a711e | |
parent | bfc9500d324bdc8da58ec8fe1f6cd8158aa79833 (diff) | |
download | monkeytype-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.ts | 100 | ||||
-rw-r--r-- | backend/src/api/controllers/user.ts | 24 | ||||
-rw-r--r-- | backend/src/api/routes/users.ts | 17 | ||||
-rw-r--r-- | backend/src/documentation/public-swagger.json | 64 | ||||
-rw-r--r-- | backend/src/middlewares/rate-limit.ts | 14 |
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, |