From 2ff1dc1c29e6a5db0cd423f1e9d9e402f13d6cc2 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Fri, 13 Jan 2023 15:26:50 +0000 Subject: [PATCH] feat: validators --- common/src/lib/schemas.ts | 21 ++- package.json | 3 +- pnpm-lock.yaml | 1 + .../framework/common/converter-failures.ts | 6 +- .../score-import/framework/orphans/orphans.ts | 26 ++- .../framework/pb/create-pb-doc.test.ts | 35 ++--- .../score-importing/score-importing.ts | 20 +-- .../score-importing/validate-score.ts | 148 ++++++++++++++++++ server/src/lib/targets/goal-utils.ts | 29 +++- .../_playtype/targets/goals/router.test.ts | 18 ++- .../_game/_playtype/targets/goals/router.ts | 6 +- .../_chartSHA256/convert-scores.test.ts | 8 +- .../charts/_chartSHA256/router.test.ts | 12 +- .../router/ir/direct-manual/router.test.ts | 65 +++++++- .../server/router/ir/fervidex/router.test.ts | 8 +- .../router/ir/usc/_playtype/router.test.ts | 2 +- .../test-utils/snapshots/snapshot-data.json | 11 +- 17 files changed, 337 insertions(+), 82 deletions(-) create mode 100644 server/src/lib/score-import/framework/score-importing/validate-score.ts diff --git a/common/src/lib/schemas.ts b/common/src/lib/schemas.ts index 604bd9efb..bc84c757d 100644 --- a/common/src/lib/schemas.ts +++ b/common/src/lib/schemas.ts @@ -403,11 +403,7 @@ const PRE_SCHEMAS = { const gptConfig = GetGamePTConfig(game, playtype); return prSchemaFnWrap({ - composedFrom: { - scorePB: "string", - lampPB: "string", - other: p.optional([{ name: "string", scoreID: "string" }]), - }, + composedFrom: [{ name: "string", scoreID: "string" }], rankingData: { rank: p.isPositiveNonZeroInteger, outOf: p.isPositiveNonZeroInteger, @@ -424,6 +420,8 @@ const PRE_SCHEMAS = { ...PR_METRICS(gptConfig.providedMetrics), ...PR_METRICS(gptConfig.derivedMetrics), + // TODO ENUMINDEXES + judgements: Object.fromEntries( gptConfig.orderedJudgements.map((j) => [j, optNull(p.isInteger)]) ), @@ -871,10 +869,19 @@ function PR_METRICS(metrics: Record, shouldAllBeOptNull const schema: PrudenceSchema = {}; for (const [key, value] of Object.entries(metrics)) { + let prValidator = PR_METRIC(value); + + if ("validate" in value) { + prValidator = p.and( + prValidator, + (self) => typeof self === "number" && value.validate(self) + ); + } + if (shouldAllBeOptNull === true) { - schema[key] = optNull(PR_METRIC(value)); + schema[key] = optNull(prValidator); } else { - schema[key] = PR_METRIC(value); + schema[key] = prValidator; } } diff --git a/package.json b/package.json index b57d94c88..d84b6dc22 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,8 @@ "@types/mongodb": "3.6.20", "@types/bson": "1.0.11", "@types/node": "16.11.7", - "@types/express-serve-static-core": "4.17.29" + "@types/express-serve-static-core": "4.17.29", + "ts-node": "10.9.1" }, "patchedDependencies": { "@types/mongodb@3.6.20": "patches/@types__mongodb@3.6.20.patch", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d8c7f476..a06a187d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,7 @@ overrides: '@types/bson': 1.0.11 '@types/node': 16.11.7 '@types/express-serve-static-core': 4.17.29 + ts-node: 10.9.1 patchedDependencies: '@types/mongodb@3.6.20': diff --git a/server/src/lib/score-import/framework/common/converter-failures.ts b/server/src/lib/score-import/framework/common/converter-failures.ts index 9911ef7a4..9945bfb39 100644 --- a/server/src/lib/score-import/framework/common/converter-failures.ts +++ b/server/src/lib/score-import/framework/common/converter-failures.ts @@ -78,6 +78,10 @@ export class InternalFailure extends ConverterFailure { } } -export function IsConverterFailure(err: ConverterFailure | Error): err is ConverterFailure { +export function IsConverterFailure(err: unknown): err is ConverterFailure { + if (err === null || typeof err !== "object") { + return false; + } + return "failureType" in err; } diff --git a/server/src/lib/score-import/framework/orphans/orphans.ts b/server/src/lib/score-import/framework/orphans/orphans.ts index 908eb213b..4ca584537 100644 --- a/server/src/lib/score-import/framework/orphans/orphans.ts +++ b/server/src/lib/score-import/framework/orphans/orphans.ts @@ -1,4 +1,5 @@ import { Converters } from "../../import-types/converters"; +import { IsConverterFailure } from "../common/converter-failures"; import { HandlePostImportSteps } from "../score-importing/score-import-main"; import { ProcessSuccessfulConverterReturn } from "../score-importing/score-importing"; import db from "external/mongo/db"; @@ -135,13 +136,24 @@ export async function ReprocessOrphan( await db["orphan-scores"].remove({ orphanID: orphan.orphanID }); // else, import the orphan. - const converterReturns = await ProcessSuccessfulConverterReturn( - orphan.userID, - res, - blacklist, - logger, - true - ); + + let converterReturns; + + try { + converterReturns = await ProcessSuccessfulConverterReturn( + orphan.userID, + res, + blacklist, + logger, + true + ); + } catch (err) { + if (IsConverterFailure(err) && err.failureType === "InvalidScore") { + return null; + } + + throw err; + } if (converterReturns === null || !converterReturns.success) { return null; diff --git a/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts b/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts index 9076a84ae..fbd1949ad 100644 --- a/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts +++ b/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts @@ -105,9 +105,8 @@ t.test("#CreatePBDoc", (t) => { t.strictSame( res, deepmerge(ExamplePBDoc, { - composedFrom: { - other: [{ name: "Best BP", scoreID: "BP_PB_ID" }], - }, + composedFrom: [{ name: "Best BP", scoreID: "BP_PB_ID" }], + scoreData: { optional: { bp: 5, @@ -251,16 +250,11 @@ t.test("#CreatePBDoc", (t) => { "Should select the best BP's BP and not the score PBs." ); - t.strictSame(res?.composedFrom, { - lampPB: "LAMP_PB_ID", - scorePB: TestingBMS7KScore.scoreID, - other: [ - { - name: "Best BP", - scoreID: "BP_PB_ID", - }, - ], - }); + t.strictSame(res?.composedFrom, [ + { name: "Best Score", scoreID: TestingBMS7KScore.scoreID }, + { name: "Best Lamp", scoreID: "LAMP_PB_ID" }, + { name: "Best BP", scoreID: "BP_PB_ID" }, + ]); t.end(); }); @@ -313,16 +307,11 @@ t.test("#CreatePBDoc", (t) => { "Should select the best BP's BP and not the score PBs." ); - t.strictSame(res?.composedFrom, { - lampPB: "LAMP_PB_ID", - scorePB: TestingBMS7KScore.scoreID, - other: [ - { - name: "Best BP", - scoreID: "BP_PB_ID", - }, - ], - }); + t.strictSame(res?.composedFrom, [ + { name: "Best Score", scoreID: TestingBMS7KScore.scoreID }, + { name: "Best Lamp", scoreID: "LAMP_PB_ID" }, + { name: "Best BP", scoreID: "BP_PB_ID" }, + ]); t.end(); }); diff --git a/server/src/lib/score-import/framework/score-importing/score-importing.ts b/server/src/lib/score-import/framework/score-importing/score-importing.ts index 54a43aa7b..9648e5ed5 100644 --- a/server/src/lib/score-import/framework/score-importing/score-importing.ts +++ b/server/src/lib/score-import/framework/score-importing/score-importing.ts @@ -1,6 +1,7 @@ import { HydrateScore } from "./hydrate-score"; import { GetScoreQueueMaybe, InsertQueue, QueueScoreInsert } from "./insert-score"; import { CreateScoreID } from "./score-id"; +import { ValidateScore } from "./validate-score"; import { IsConverterFailure } from "../common/converter-failures"; import { OrphanScore } from "../orphans/orphans"; import db from "external/mongo/db"; @@ -120,11 +121,12 @@ export async function ImportIterableDatapoint( blacklist: Array, logger: KtLogger ): Promise { - // Converter Function Return - let cfnReturn: ConverterFnSuccessReturn; - try { - cfnReturn = await ConverterFunction(data, context, importType, logger); + const cfnReturn = await ConverterFunction(data, context, importType, logger); + + const res = await ProcessSuccessfulConverterReturn(userID, cfnReturn, blacklist, logger); + + return res; } catch (e) { const err = e as ConverterFailure | Error; @@ -237,8 +239,6 @@ export async function ImportIterableDatapoint( } } } - - return ProcessSuccessfulConverterReturn(userID, cfnReturn, blacklist, logger); } export async function ProcessSuccessfulConverterReturn( @@ -248,7 +248,7 @@ export async function ProcessSuccessfulConverterReturn( logger: KtLogger, forceImmediateImport = false ): Promise { - const result = await HydrateAndInsertScore( + const result = await HydrateCheckAndInsertScore( userID, cfnReturn.dryScore, cfnReturn.chart, @@ -278,7 +278,7 @@ export async function ProcessSuccessfulConverterReturn( } /** - * Hydrates and inserts a score to the Tachi database. + * Hydrates, validates and inserts a score to the Tachi database. * @param userID - The user this score is from. * @param dryScore - The score that is to be hydrated and inserted. * @param chart - The chart this score is on. @@ -288,7 +288,7 @@ export async function ProcessSuccessfulConverterReturn( * @param force - Whether to immediately insert the score into the database * or not. */ -async function HydrateAndInsertScore( +async function HydrateCheckAndInsertScore( userID: integer, dryScore: DryScore, chart: ChartDocument, @@ -336,6 +336,8 @@ async function HydrateAndInsertScore( const score = HydrateScore(userID, dryScore, chart, song, scoreID, logger); + ValidateScore(score, chart); + let res; if (force) { diff --git a/server/src/lib/score-import/framework/score-importing/validate-score.ts b/server/src/lib/score-import/framework/score-importing/validate-score.ts new file mode 100644 index 000000000..36be6ef9e --- /dev/null +++ b/server/src/lib/score-import/framework/score-importing/validate-score.ts @@ -0,0 +1,148 @@ +import { InvalidScoreFailure } from "../common/converter-failures"; +import { GPT_SERVER_IMPLEMENTATIONS } from "game-implementations/game-implementations"; +import { GetGPTConfig, GetGPTString } from "tachi-common"; +import type { ChartDocument, GPTString, ScoreDocument } from "tachi-common"; +import type { ConfScoreMetric } from "tachi-common/types/metrics"; + +/** + * Checks if a score passes all of its validation checks. This tests gpt specific + * things, like whether this passes the validators defined in gptConfig, or whether + * it passes the chart-dependent validators defined in the gpt server implementation. + * + * @returns nothing. This will throw an InvalidScoreFailure on error. + */ +export function ValidateScore(score: ScoreDocument, chart: ChartDocument): void { + const gptString = GetGPTString(score.game, score.playtype); + const gptConfig = GetGPTConfig(gptString); + + const errs: Array = []; + + ValidateMetrics( + errs, + gptConfig.providedMetrics, + gptString, + score, + chart, + // @ts-expect-error ughhh + (s, m) => s.scoreData[m] + ); + ValidateMetrics( + errs, + gptConfig.derivedMetrics, + gptString, + score, + chart, + // @ts-expect-error ughhh + (s, m) => s.scoreData[m] + ); + + ValidateMetrics( + errs, + gptConfig.optionalMetrics, + gptString, + score, + chart, + // @ts-expect-error ughhh + (s, m) => s.scoreData.optional[m], + true + ); + + if (errs.length > 0) { + const errorStr = errs.length === 1 ? "error" : "errors"; + + throw new InvalidScoreFailure(`Got ${errs.length} ${errorStr} when validating score: +${errs.join("\n")}`); + } +} + +function ValidateMetrics( + errs: Array, + metrics: Record, + gptString: GPTString, + score: ScoreDocument, + chart: ChartDocument, + valueGetter: (s: ScoreDocument, metric: string) => any, + optional?: boolean +) { + const gptImpl = GPT_SERVER_IMPLEMENTATIONS[gptString]; + + for (const [metric, conf] of Object.entries(metrics)) { + const scoreVal: any = valueGetter(score, metric); + + if (optional && scoreVal === undefined) { + continue; + } + + switch (conf.type) { + case "ENUM": { + if (!conf.values.includes(scoreVal)) { + errs.push( + `Invalid value for ${metric}, got ${scoreVal}, but expected any of ${conf.values.join( + ", " + )}.` + ); + } + + break; + } + + case "INTEGER": + case "DECIMAL": { + if (conf.type === "INTEGER" && !Number.isSafeInteger(scoreVal)) { + errs.push( + `Invalid value for ${metric}, got ${scoreVal}, but expected an integer.` + ); + } else if (!Number.isFinite(scoreVal)) { + errs.push( + `Invalid value for ${metric}, got ${scoreVal}, but expected a finite number.` + ); + } + + let err: string | true; + + if (conf.chartDependentMax) { + // @ts-expect-error hack, this is fine. don't worry. + err = gptImpl.validators[metric](scoreVal, chart); + } else { + err = conf.validate(scoreVal); + } + + if (typeof err === "string") { + errs.push(`Invalid value for ${metric}, ${err}`); + } + + break; + } + + case "GRAPH": + case "NULLABLE_GRAPH": { + if (!Array.isArray(scoreVal)) { + errs.push(`Invalid value for metric ${metric}, expected an array.`); + break; + } + + for (const v of scoreVal) { + if (conf.type === "NULLABLE_GRAPH" && v === null) { + continue; + } + + const err = conf.validate(v); + + if (typeof err === "string") { + errs.push(`Invalid value for metric ${metric}, ${err}, got ${v}.`); + } + } + + if (conf.size) { + const err = conf.size(scoreVal.length); + + if (typeof err === "string") { + errs.push( + `Invalid size of metric ${metric}, ${err}, got an array of size ${scoreVal.length}.` + ); + } + } + } + } + } +} diff --git a/server/src/lib/targets/goal-utils.ts b/server/src/lib/targets/goal-utils.ts index 59f213a35..0903116ab 100644 --- a/server/src/lib/targets/goal-utils.ts +++ b/server/src/lib/targets/goal-utils.ts @@ -10,7 +10,10 @@ import { import { GetFolderForIDGuaranteed, HumaniseChartID } from "utils/db"; import { GetFolderChartIDs } from "utils/folder"; import { FormatMaxDP, HumanisedJoinArray } from "utils/misc"; -import type { GoalCriteriaFormatter } from "game-implementations/types"; +import type { + ChartSpecificMetricValidator, + GoalCriteriaFormatter, +} from "game-implementations/types"; import type { GPTString, Game, GoalDocument, Playtype } from "tachi-common"; export async function CreateGoalTitle( @@ -146,6 +149,8 @@ function FormatCriteria( if (v === undefined) { throw new Error(`Invalid criteria value '${criteria.value}'.`); } + + return v; } else if (conf.type === "DECIMAL" || conf.type === "INTEGER") { const fmt: GoalCriteriaFormatter | undefined = // @ts-expect-error it still thinks criteria.key might be a symbol. @@ -288,19 +293,29 @@ export async function ValidateGoalChartsAndCriteria( ); } - let validateFn: (v: number) => string | true; + let err; if (config.chartDependentMax) { + const chart = await db.anyCharts[game].findOne({ + playtype, + // guaranteed by previous if statement + chartID: charts.data as string, + }); + + if (!chart) { + throw new Error( + `Chart ${charts.data} was removed from the database while a goal was being validated on it?` + ); + } + // @ts-expect-error this is fine leave me alone - validateFn = gptImpl.validators[criteria.key]!; + err = gptImpl.validators[criteria.key](criteria.value, chart); } else { - validateFn = config.validate; + err = config.validate(criteria.value); } - const err = validateFn(criteria.value); - if (err !== true) { - throw new Error(`Invalid value ${criteria.value} for ${criteria.key}, ${err}.`); + throw new Error(`Invalid value ${criteria.value} for ${criteria.key}, ${err}`); } break; diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.test.ts index c1fd20ca8..fa42c3774 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.test.ts @@ -314,11 +314,13 @@ t.test("POST /api/v1/users/:userID/games/:game/:playtype/targets/add-goal", asyn value: -1, }, }), - mkInput("percent of 0 is a non-goal", { - criteria: { - value: 0, - }, - }), + // as stupid as it is, it technically "is" a goal, as it's a valid + // value for percent. + // mkInput("percent of 0 is a non-goal", { + // criteria: { + // value: 0, + // }, + // }), mkInput("percent greater than 100", { criteria: { value: 100.1, @@ -330,6 +332,12 @@ t.test("POST /api/v1/users/:userID/games/:game/:playtype/targets/add-goal", asyn value: IIDX_GRADES.MAX + 1, }, }), + mkInput("string enum", { + criteria: { + key: "grade", + value: IIDX_GRADES.MAX.toString(), + }, + }), mkInput("invalid grade", { criteria: { key: "grade", diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts index d514ca6e5..0f376a07d 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts @@ -156,13 +156,15 @@ router.post( const validCriteria = [ ...Object.keys(gptConfig.providedMetrics), - Object.keys(gptConfig.derivedMetrics), + ...Object.keys(gptConfig.derivedMetrics), ]; if (!validCriteria.includes(req.body.criteria.key)) { return res.status(400).json({ success: false, - description: `Invalid criteria, expected any of ${validCriteria.join(", ")}.`, + description: `Invalid criteria '${ + req.body.criteria.key + }', expected any of ${validCriteria.join(", ")}.`, }); } diff --git a/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.test.ts b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.test.ts index 72f7ac8ab..76902528f 100644 --- a/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.test.ts +++ b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.test.ts @@ -6,11 +6,11 @@ import { BMSGazerChart, GetKTDataJSON } from "test-utils/test-data"; import type { PBScoreDocument } from "tachi-common"; const pbScore = { - composedFrom: { - lampPB: "mock_lampPB", - }, + composedFrom: [{ name: "Best Score", scoreID: "mock_lampPB" }], scoreData: { - lampIndex: 4, + enumIndexes: { + lamp: 4, + }, score: 1234, optional: {}, }, diff --git a/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.test.ts b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.test.ts index 29c891a16..40f147d1c 100644 --- a/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.test.ts +++ b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.test.ts @@ -24,13 +24,15 @@ t.test("GET /ir/beatoraja/charts/:chartSHA256/scores", (t) => { t.test("Should return PB scores on a chart", async (t) => { // very lazy fake scores await db["personal-bests"].insert({ - composedFrom: { - lampPB: "mock_lampPB", - }, + composedFrom: [{ name: "Best Lamp", scoreID: "mock_lampPB" }], scoreData: { - lampIndex: 4, score: 1234, - optional: {}, + enumIndexes: { + lamp: 4, + }, + optional: { + enumIndexes: {}, + }, }, scoreMeta: {}, chartID: GAZER_CHARTID, diff --git a/server/src/server/router/ir/direct-manual/router.test.ts b/server/src/server/router/ir/direct-manual/router.test.ts index 663404637..8ed83d637 100644 --- a/server/src/server/router/ir/direct-manual/router.test.ts +++ b/server/src/server/router/ir/direct-manual/router.test.ts @@ -166,7 +166,70 @@ t.test("POST /ir/direct-manual/import", async (t) => { t.equal(res.body.success, false, "Should not be successful"); t.match( res.body.description, - /Invalid BATCH-MANUAL: scores\[0\].score \| Expected a positive integer. \| Received 123\.5/iu + /Invalid BATCH-MANUAL: scores\[0\].score \| Expected an integer. \| Received 123\.5/iu + ); + + t.end(); + }); + + t.test("Should apply chart specific validators for scores.", async (t) => { + const bmScore: BatchManualScore = { + identifier: "1", + lamp: "CLEAR", + matchType: "tachiSongID", + difficulty: "ANOTHER", + // greater than the max possible score on this chart + score: 9000, + }; + + const res = await mockApi + .post("/ir/direct-manual/import") + .set("Authorization", `Bearer mock_token`) + .send(deepmerge(baseBatchManual, { scores: [bmScore] })); + + t.equal(res.body.success, true, "Should be successful but with one failed score"); + t.strictSame( + res.body.body.errors, + [ + { + type: "InvalidDatapoint", + message: `Got 1 error when validating score: +Invalid value for score, EX Score cannot be greater than 1572 for this chart. Got 9000.`, + }, + ], + "Should have one failed score." + ); + + t.end(); + }); + + t.test("Should reject negative values for jubeat scores.", async (t) => { + const bmScore: BatchManualScore = { + identifier: "1", + lamp: "CLEAR", + matchType: "tachiSongID", + difficulty: "EXT", + score: -100, + musicRate: 50, + }; + + const res = await mockApi + .post("/ir/direct-manual/import") + .set("Authorization", `Bearer mock_token`) + .send( + deepmerge(baseBatchManual, { + scores: [bmScore], + meta: { + game: "jubeat", + playtype: "Single", + }, + }) + ); + + t.equal(res.body.success, false, "Should not be successful"); + t.match( + res.body.description, + "Invalid BATCH-MANUAL: scores[0].score | Expected a number between 0 and 1000000. | Received -100 [type: number]." ); t.end(); diff --git a/server/src/server/router/ir/fervidex/router.test.ts b/server/src/server/router/ir/fervidex/router.test.ts index 3b67fc8dc..eed98a248 100644 --- a/server/src/server/router/ir/fervidex/router.test.ts +++ b/server/src/server/router/ir/fervidex/router.test.ts @@ -233,7 +233,7 @@ t.test("POST /ir/fervidex/class/submit", (t) => { const ugs = await db["game-stats"].findOne({ userID: 1, game: "iidx", playtype: "SP" }); - t.equal(ugs?.classes.dan, 18); + t.equal(ugs?.classes.dan, "KAIDEN"); const recentAchievement = await db["class-achievements"].findOne({ userID: 1, @@ -267,7 +267,7 @@ t.test("POST /ir/fervidex/class/submit", (t) => { const ugs = await db["game-stats"].findOne({ userID: 1, game: "iidx", playtype: "DP" }); - t.equal(ugs?.classes.dan, 17); + t.equal(ugs?.classes.dan, "CHUUDEN"); t.end(); }); @@ -479,7 +479,7 @@ t.test("POST /ir/fervidex/profile/submit", (t) => { playtype: "SP", }); - t.equal(ugs!.classes.dan, 15, "Should successfully update dan to 9th."); + t.equal(ugs!.classes.dan, "DAN_9", "Should successfully update dan to 9th."); t.end(); }); @@ -520,7 +520,7 @@ t.test("POST /ir/fervidex/profile/submit", (t) => { playtype: "SP", }); - t.equal(ugs!.classes.dan, 15, "Should successfully update dan to 9th."); + t.equal(ugs!.classes.dan, "DAN_9", "Should successfully update dan to 9th."); const dbRes = await db["fer-settings"].findOne({ userID: 1 }); diff --git a/server/src/server/router/ir/usc/_playtype/router.test.ts b/server/src/server/router/ir/usc/_playtype/router.test.ts index 5624ed61b..61bad2ceb 100644 --- a/server/src/server/router/ir/usc/_playtype/router.test.ts +++ b/server/src/server/router/ir/usc/_playtype/router.test.ts @@ -307,7 +307,7 @@ t.test("GET /charts/:chartHash/leaderboard", (t) => { score: 8_000_000, }, rankingData: { rank: 2 }, - composedFrom: { scorePB: "other_usc_score_pb" }, + composedFrom: [{ name: "Best Score", scoreID: "other_usc_score_pb" }], }), ]); diff --git a/server/src/test-utils/snapshots/snapshot-data.json b/server/src/test-utils/snapshots/snapshot-data.json index ff745fc5b..9c4502290 100644 --- a/server/src/test-utils/snapshots/snapshot-data.json +++ b/server/src/test-utils/snapshots/snapshot-data.json @@ -1,7 +1,7 @@ { "Invalid Goal: negative percent": "[criteria.value] Expected number to be greater than or equal to 0. (Received -1)", "Invalid Goal: percent of 0 is a non-goal": "Invalid value of 0 for percent goal. Percents must be between 0 and 100.", - "Invalid Goal: percent greater than 100": "Invalid value of 100.1 for percent goal. Percents must be between 0 and 100.", + "Invalid Goal: percent greater than 100": "Invalid value 100.1 for percent, Expected a number between 0 and 100.", "Invalid Goal: abs without countNum": "[criteria.countNum] Expected number to be greater than or equal to 0. (Received nothing (undefined))", "Invalid Goal: proportion without countNum": "[criteria.countNum] Expected number to be greater than or equal to 0. (Received nothing (undefined))", "Invalid Goal: single with countNum": "[criteria.countNum] Invalid countNum for mode 'single'. Must not have one! (Received 123)", @@ -15,7 +15,7 @@ "Invalid Goal: charts.data array of chartIDs that don't exist": "Expected charts.data to match 2 charts. Instead, it only matched 1. Are all of these chartIDs valid?", "Invalid Goal: charts.data folder refers to folder that doesn't exist": "A folder with id fake-folder does not exist for iidx:SP.", "Invalid Goal: nonsense charts.data": "A chart with id nonsense does not exist for iidx:SP.", - "Invalid Goal: multi-score for iidx is illegal": "Invalid key for iidx with multiple charts. Creating score goals on multiple charts where score is relative to notecount is a terrible idea, and has been disabled.", + "Invalid Goal: multi-score for iidx is illegal": "Creating score goals on multiple charts where the maximum value is relative to the chart is a terrible idea, and has been disabled.", "Invalid Goal: too big grade": "Invalid value of 10 for grade goal. No such grade exists at that index.", "Invalid Goal: invalid grade": "Invalid value of 0.5 for grade goal. No such grade exists at that index.", "Invalid Goal: too big lamp": "Invalid value of 8 for lamp goal. No such lamp exists at that index.", @@ -24,11 +24,12 @@ "Invalid Goal: proportion with greater than 1 countNum": "Invalid countNum for goal with criteria.mode of 'proportion'. Expected a decimal in (0, 1]", "Invalid Goal: proportion which results in countNum of 0": "countNum (0.05) is too small for a goal with 2 charts. Would result in requiring 0 charts to achieve the goal.", "Invalid Goal: charts.data array of chartIDs where some don't exist": "Expected charts.data to match 2 charts. Instead, it only matched 1. Are all of these chartIDs valid?", - "Invalid Goal: folder-score for iidx is illegal": "Invalid key for iidx with multiple charts. Creating score goals on multiple charts where score is relative to notecount is a terrible idea, and has been disabled.", + "Invalid Goal: folder-score for iidx is illegal": "Creating score goals on multiple charts where the maximum value is relative to the chart is a terrible idea, and has been disabled.", "Invalid Goal: any-score for iidx is illegal": "Invalid key for iidx with multiple charts. Creating score goals on multiple charts where score is relative to notecount is a terrible idea, and has been disabled.", "Invalid Goal: charts single but criteria not.": "Invalid countNum for goal with criteria.mode of 'absolute'. Expected a whole number less than the total amount of charts available and greater than 1. (Got 2, while total charts was 1.)", - "Invalid Goal: too big score": "Invalid value of 9000 for goal. Maximum score possible on this chart is 1572.", + "Invalid Goal: too big score": "Invalid value 9000 for score, EX Score cannot be greater than 1572 for this chart. Got 9000.", "Invalid Goal: LampIndex:Multi:Proportion Case": "Criteria Mode must be 'single' or 'absolute' if Charts Type is 'multi'. Doesn't make sense to have proportional goals when you're capped at 10 charts.", "Invalid Goal: GradeIndex:Multi:Proportion Case": "Criteria Mode must be 'single' or 'absolute' if Charts Type is 'multi'. Doesn't make sense to have proportional goals when you're capped at 10 charts.", - "Invalid Goal: Percent:Multi:Proportion Case": "Criteria Mode must be 'single' or 'absolute' if Charts Type is 'multi'. Doesn't make sense to have proportional goals when you're capped at 10 charts." + "Invalid Goal: Percent:Multi:Proportion Case": "Criteria Mode must be 'single' or 'absolute' if Charts Type is 'multi'. Doesn't make sense to have proportional goals when you're capped at 10 charts.", + "Invalid Goal: string enum": "[criteria.value] Expected number to be greater than or equal to 0. (Received 9)" } \ No newline at end of file