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:
Christian Fehmer 2024-06-24 13:55:13 +02:00 committed by GitHub
parent bfc9500d32
commit 442153724a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 202 additions and 17 deletions

View file

@ -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[] {

View file

@ -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);
}

View file

@ -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;

View file

@ -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
}
}
}
}
}

View file

@ -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,