mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-08 06:48:04 +03:00
feat: validators
This commit is contained in:
@@ -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<string, ConfScoreMetric>, 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
Generated
+1
@@ -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':
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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<D, C>(
|
||||
blacklist: Array<string>,
|
||||
logger: KtLogger
|
||||
): Promise<ImportProcessingInfo | null> {
|
||||
// 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<D, C>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ProcessSuccessfulConverterReturn(userID, cfnReturn, blacklist, logger);
|
||||
}
|
||||
|
||||
export async function ProcessSuccessfulConverterReturn(
|
||||
@@ -248,7 +248,7 @@ export async function ProcessSuccessfulConverterReturn(
|
||||
logger: KtLogger,
|
||||
forceImmediateImport = false
|
||||
): Promise<ImportProcessingInfo | null> {
|
||||
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) {
|
||||
|
||||
@@ -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<string> = [];
|
||||
|
||||
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<string>,
|
||||
metrics: Record<string, ConfScoreMetric>,
|
||||
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}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<GPT extends GPTString>(
|
||||
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;
|
||||
|
||||
+13
-5
@@ -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",
|
||||
|
||||
+4
-2
@@ -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(", ")}.`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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" }],
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
Reference in New Issue
Block a user