From 05692d64760f2f9cb0705ff2a7e078e170c2322b Mon Sep 17 00:00:00 2001 From: zkldi Date: Thu, 17 Jun 2021 01:07:36 +0100 Subject: [PATCH] test user endpoints --- .../games/_game/_playtype/router.test.ts | 319 +++++++++++++++++- .../_userID/games/_game/_playtype/router.ts | 30 +- server/src/server/server.ts | 1 + server/src/test-utils/mock-db/game-stats.json | 10 +- server/src/utils/user.ts | 25 +- 5 files changed, 363 insertions(+), 22 deletions(-) diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts index f8914ea69..be8115353 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts @@ -1,15 +1,330 @@ import t from "tap"; +import db from "../../../../../../../../../external/mongo/db"; import { CloseAllConnections } from "../../../../../../../../../test-utils/close-connections"; import mockApi from "../../../../../../../../../test-utils/mock-api"; import ResetDBState from "../../../../../../../../../test-utils/resets"; +import { + ScoreDocument, + UserGoalDocument, + GoalDocument, + MilestoneDocument, + UserMilestoneDocument, + PBScoreDocument, +} from "tachi-common"; +import { Testing511Song, Testing511SPA } from "../../../../../../../../../test-utils/test-data"; t.test("GET /api/v1/users/:userID/games/:game/:playtype/", (t) => { t.beforeEach(ResetDBState); t.test("Should return a users statistics for that game.", async (t) => { - const res = await mockApi.get("/api/v1/users/zkldi/games/iidx/SP"); + const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP"); - t.strictSame(res.body, {}); + t.hasStrict(res.body, { + success: true, + description: "Retrieved user statistics for test_zkldi (iidx SP)", + body: { + gameStats: { + userID: 1, + game: "iidx", + playtype: "SP", + classes: {}, + ratings: {}, + }, + firstScore: null, + mostRecentScore: null, + totalScores: 1, + rankingData: { + ranking: 1, + outOf: 1, + }, + }, + }); + + t.end(); + }); + + t.test("Should return a users first score if one exists.", async (t) => { + await db.scores.insert([ + { + userID: 1, + timeAchieved: 100, + scoreID: "foo", + game: "iidx", + playtype: "SP", + }, + { + userID: 1, + timeAchieved: 200, + scoreID: "bar", + game: "iidx", + playtype: "SP", + }, + { + userID: 1, + timeAchieved: 300, + scoreID: "baz", + game: "iidx", + playtype: "SP", + }, + ] as ScoreDocument[]); + + const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP"); + + t.hasStrict(res.body, { + success: true, + description: "Retrieved user statistics for test_zkldi (iidx SP)", + body: { + gameStats: { + userID: 1, + game: "iidx", + playtype: "SP", + classes: {}, + ratings: {}, + }, + firstScore: { + timeAchieved: 100, + scoreID: "foo", + }, + mostRecentScore: { + timeAchieved: 300, + scoreID: "baz", + }, + totalScores: 4, + rankingData: { + ranking: 1, + outOf: 1, + }, + }, + }); + + t.end(); + }); + + t.end(); +}); + +t.test("GET /api/v1/users/:userID/games/:game/:playtype/goals", (t) => { + t.beforeEach(ResetDBState); + + t.test( + "Should return all of a users goals, and only unachieved goals if the argument is set.", + async (t) => { + await db["user-goals"].insert([ + { + goalID: "foo", + userID: 1, + game: "iidx", + playtype: "SP", + achieved: false, + }, + { + goalID: "bar", + userID: 1, + game: "iidx", + playtype: "SP", + achieved: true, + }, + ] as UserGoalDocument[]); + + await db.goals.insert([{ goalID: "foo" }, { goalID: "bar" }] as GoalDocument[]); + + const res = await mockApi.get("/api/v1/users/1/games/iidx/SP/goals"); + + t.hasStrict(res.body, { + success: true, + description: "Successfully returned 2 goal(s).", + body: { + userGoals: [ + { + goalID: "foo", + achieved: false, + }, + { + goalID: "bar", + achieved: true, + }, + ], + goals: [{ goalID: "bar" }, { goalID: "foo" }], + }, + }); + + const resUnachieved = await mockApi.get( + "/api/v1/users/1/games/iidx/SP/goals?unachieved=true" + ); + + t.hasStrict(resUnachieved.body, { + success: true, + description: "Successfully returned 1 goal(s).", + body: { + userGoals: [ + { + goalID: "foo", + achieved: false, + }, + ], + goals: [{ goalID: "foo" }], + }, + }); + + t.end(); + } + ); + + t.end(); +}); + +t.test("GET /api/v1/users/:userID/games/:game/:playtype/milestones", (t) => { + t.beforeEach(ResetDBState); + + t.test( + "Should return all of a users milestones, and only unachieved milestones if the argument is set.", + async (t) => { + await db["user-milestones"].insert([ + { + milestoneID: "foo", + userID: 1, + game: "iidx", + playtype: "SP", + achieved: false, + }, + { + milestoneID: "bar", + userID: 1, + game: "iidx", + playtype: "SP", + achieved: true, + }, + ] as UserMilestoneDocument[]); + + await db.milestones.insert([ + { milestoneID: "foo" }, + { milestoneID: "bar" }, + ] as MilestoneDocument[]); + + const res = await mockApi.get("/api/v1/users/1/games/iidx/SP/milestones"); + + t.hasStrict(res.body, { + success: true, + description: "Successfully returned 2 milestone(s).", + body: { + userMilestones: [ + { + milestoneID: "foo", + achieved: false, + }, + { + milestoneID: "bar", + achieved: true, + }, + ], + milestones: [{ milestoneID: "bar" }, { milestoneID: "foo" }], + }, + }); + + const resUnachieved = await mockApi.get( + "/api/v1/users/1/games/iidx/SP/milestones?unachieved=true" + ); + + t.hasStrict(resUnachieved.body, { + success: true, + description: "Successfully returned 1 milestone(s).", + body: { + userMilestones: [ + { + milestoneID: "foo", + achieved: false, + }, + ], + milestones: [{ milestoneID: "foo" }], + }, + }); + + t.end(); + } + ); + + t.end(); +}); + +t.test("GET /api/v1/users/:userID/games/:game/:playtype/recent-scores", (t) => { + t.beforeEach(ResetDBState); + + t.test("Should return a users 100 most recent scores.", async (t) => { + const mockScores: ScoreDocument[] = []; + + for (let i = 0; i < 200; i++) { + mockScores.push({ + userID: 1, + game: "iidx", + playtype: "SP", + timeAchieved: i * 100, + scoreID: `scoreID_${i}`, + chartID: Testing511SPA.chartID, + songID: Testing511Song.id, + } as ScoreDocument); + } + + await db.scores.insert(mockScores); + + for (const sc of mockScores) { + delete sc._id; // lol + } + + const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/recent-scores"); + + t.hasStrict(res.body, { + success: true, + description: "Retrieved 100 scores.", + body: { + scores: mockScores.slice(100).reverse(), + songs: [Testing511Song], + charts: [Testing511SPA], + }, + }); + + t.end(); + }); + + t.end(); +}); + +t.test("GET /api/v1/users/:userID/games/:game/:playtype/best", (t) => { + t.beforeEach(ResetDBState); + + t.test("Should return a users 100 most recent scores.", async (t) => { + const mockPBs: PBScoreDocument[] = []; + + for (let i = 0; i < 200; i++) { + mockPBs.push({ + userID: 1, + game: "iidx", + playtype: "SP", + isPrimary: true, + chartID: i.toString(), // hack to generate some random chartIDs + songID: Testing511Song.id, + calculatedData: { + ktRating: i, + }, + } as PBScoreDocument); + } + + await db["personal-bests"].insert(mockPBs); + + for (const sc of mockPBs) { + delete sc._id; // lol + } + + const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/best"); + + t.hasStrict(res.body, { + success: true, + description: "Retrieved 100 scores.", + body: { + scores: mockPBs.slice(100).reverse(), + songs: [Testing511Song], + charts: [], + }, + }); t.end(); }); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts index eeeb7035a..8cc91d11b 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts @@ -4,6 +4,8 @@ import { SYMBOL_TachiData } from "../../../../../../../../../lib/constants/tachi import { GetRelevantSongsAndCharts } from "../../../../../../../../../utils/db"; import { GetDefaultScoreRatingAlg, GetUsersRanking } from "../../../../../../../../../utils/user"; import { CheckUserPlayedGamePlaytype } from "./middleware"; +import { FilterQuery } from "mongodb"; +import { UserGoalDocument, UserMilestoneDocument } from "tachi-common"; const router: Router = Router({ mergeParams: true }); @@ -30,6 +32,7 @@ router.get("/", async (req, res) => { userID: user.id, game, playtype, + timeAchieved: { $ne: null }, }, { sort: { @@ -42,6 +45,7 @@ router.get("/", async (req, res) => { userID: user.id, game, playtype, + timeAchieved: { $ne: null }, }, { sort: { @@ -76,12 +80,17 @@ router.get("/goals", async (req, res) => { const game = req[SYMBOL_TachiData]!.game!; const playtype = req[SYMBOL_TachiData]!.playtype!; - const userGoals = await db["user-goals"].find({ + const query: FilterQuery = { userID: user.id, game, playtype, - achieved: req.query.unachieved ? false : undefined, - }); + }; + + if (req.query.unachieved) { + query.achieved = false; + } + + const userGoals = await db["user-goals"].find(query); const goals = await db.goals.find({ goalID: { $in: userGoals.map((e) => e.goalID) }, @@ -89,7 +98,7 @@ router.get("/goals", async (req, res) => { return res.status(200).json({ success: true, - description: `Successfully returned ${userGoals.length} goals.`, + description: `Successfully returned ${userGoals.length} goal(s).`, body: { userGoals, goals, @@ -108,12 +117,17 @@ router.get("/milestones", async (req, res) => { const game = req[SYMBOL_TachiData]!.game!; const playtype = req[SYMBOL_TachiData]!.playtype!; - const userMilestones = await db["user-milestones"].find({ + const query: FilterQuery = { userID: user.id, game, playtype, - achieved: req.query.unachieved ? false : undefined, - }); + }; + + if (req.query.unachieved) { + query.achieved = false; + } + + const userMilestones = await db["user-milestones"].find(query); const milestones = await db.milestones.find({ milestoneID: { $in: userMilestones.map((e) => e.milestoneID) }, @@ -121,7 +135,7 @@ router.get("/milestones", async (req, res) => { return res.status(200).json({ success: true, - description: `Successfully returned ${userMilestones.length} milestones.`, + description: `Successfully returned ${userMilestones.length} milestone(s).`, body: { userMilestones, milestones, diff --git a/server/src/server/server.ts b/server/src/server/server.ts index cdc79f652..02e18d539 100644 --- a/server/src/server/server.ts +++ b/server/src/server/server.ts @@ -79,6 +79,7 @@ interface ExpressJSONErr extends SyntaxError { // eslint-disable-next-line @typescript-eslint/no-unused-vars const MAIN_ERR_HANDLER: express.ErrorRequestHandler = (err, req, res) => { + logger.info("foo"); if (err instanceof SyntaxError) { const expErr: ExpressJSONErr = err as ExpressJSONErr; if (expErr.status === 400 && "body" in expErr) { diff --git a/server/src/test-utils/mock-db/game-stats.json b/server/src/test-utils/mock-db/game-stats.json index 0637a088a..1d228d3ce 100644 --- a/server/src/test-utils/mock-db/game-stats.json +++ b/server/src/test-utils/mock-db/game-stats.json @@ -1 +1,9 @@ -[] \ No newline at end of file +[ + { + "userID": 1, + "game": "iidx", + "playtype": "SP", + "classes": {}, + "ratings": {} + } +] \ No newline at end of file diff --git a/server/src/utils/user.ts b/server/src/utils/user.ts index c8bfec069..b47fc6415 100644 --- a/server/src/utils/user.ts +++ b/server/src/utils/user.ts @@ -163,16 +163,19 @@ export async function GetUsersRanking(stats: UserGameStats) { }, }, { - total: { $sum: 1 }, - ranking: { - $sum: { - $cond: { - if: { - // @ts-expect-error garbage... - $gte: [`$ratings.${ratingKey}`, stats.ratings[ratingKey]], + $group: { + _id: null, + outOf: { $sum: 1 }, + ranking: { + $sum: { + $cond: { + if: { + // @ts-expect-error garbage... + $gte: [`$ratings.${ratingKey}`, stats.ratings[ratingKey]], + }, + then: 1, + else: 0, }, - then: 1, - else: 0, }, }, }, @@ -180,7 +183,7 @@ export async function GetUsersRanking(stats: UserGameStats) { ]); return { - ranking: aggRes.ranking, - total: aggRes.total, + ranking: (aggRes[0].ranking + 1) as integer, + outOf: aggRes[0].outOf as integer, }; }