mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2026-02-25 11:46:02 +08:00
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
This commit is contained in:
parent
bfc9500d32
commit
442153724a
5 changed files with 202 additions and 17 deletions
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue