diff --git a/server/src/server/router/ir/beatoraja/charts/convert-scores.test.ts b/server/src/server/router/ir/beatoraja/charts/convert-scores.test.ts index 08cbf7e66..4c6efab2a 100644 --- a/server/src/server/router/ir/beatoraja/charts/convert-scores.test.ts +++ b/server/src/server/router/ir/beatoraja/charts/convert-scores.test.ts @@ -178,7 +178,7 @@ t.test("#KtchiPBScoreToBeatorajaFormat", (t) => { await db.scores.remove({ scoreID: "mock_lampPB" }); t.rejects(() => KtchiPBScoreToBeatorajaFormat(pbScore, gazerChart, 2), { - message: /User's PB on.*has no lampPB/u, + message: /User 1's PB on.*has no lampPB/u, } as any); t.end(); diff --git a/server/src/server/router/ir/beatoraja/router.test.ts b/server/src/server/router/ir/beatoraja/router.test.ts index 64d068ff1..0e1165b2e 100644 --- a/server/src/server/router/ir/beatoraja/router.test.ts +++ b/server/src/server/router/ir/beatoraja/router.test.ts @@ -199,4 +199,277 @@ t.test("POST /ir/beatoraja/submit-score", (t) => { t.end(); }); +const courseScore = { + course: { + name: "GENOSIDE 2018 段位認定 発狂皆伝", + charts: [ + { + md5: "cfad3baadce9e02c45021963453d7c94", + }, + { + md5: "77d23be22b2370925c573d922276bce0", + }, + { + md5: "188a99f74ab71804f2e360dcf484545c", + }, + { + md5: "c46a81cb184f5a804c119930d6eba748", + }, + ], + constraint: ["MIRROR", "GAUGE_LR2", "LN"], + trophy: [{}, {}], + lntype: 0, + }, + score: { + sha256: "", + lntype: 0, + player: "unknown", + clear: "Clear", + date: 0, + epg: 1334, + lpg: 788, + egr: 1634, + lgr: 382, + egd: 239, + lgd: 142, + ebd: 34, + lbd: 8, + epr: 0, + lpr: 93, + ems: 63, + lms: 74, + maxcombo: 225, + notes: 6005, + passnotes: 4654, + minbp: 1623, + option: 0, + assist: 0, + gauge: 0, + deviceType: "BM_CONTROLLER", + judgeAlgorithm: "Combo", + rule: "Beatoraja_7", + exscore: 6260, + }, +}; + +t.test("POST /ir/beatoraja/submit-course", (t) => { + t.beforeEach(ResetDBState); + + t.test("Should accept a valid clear", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-KtchiIR-Version", "2.0.0") + .set("Authorization", "Bearer token") + .send(courseScore); + + t.equal(res.status, 200); + t.equal(res.body.success, true); + t.equal(res.body.description, "Successfully updated class."); + + t.end(); + }); + + t.test("Should silently reject a fail", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-KtchiIR-Version", "2.0.0") + .set("Authorization", "Bearer token") + .send(deepmerge(courseScore, { score: { clear: "Failed" } })); + + t.equal(res.status, 200); + t.equal(res.body.success, true); + t.equal(res.body.description, "Class not updated."); + + t.end(); + }); + + t.test("Should reject scores with no charts", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-KtchiIR-Version", "2.0.0") + .set("Authorization", "Bearer token") + .send(deepmerge(courseScore, { course: { charts: [] } }, { arrayMerge: (d, s) => s })); + + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Course Submission."); + + t.end(); + }); + + t.test("Should reject scores with invalid chart documents", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-KtchiIR-Version", "2.0.0") + .set("Authorization", "Bearer token") + .send(deepmerge(courseScore, { course: { charts: [1, 2, 3, 4] } })); + + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Course Submission."); + + t.end(); + }); + + t.test("Should reject scores with too many chart documents", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-KtchiIR-Version", "2.0.0") + .set("Authorization", "Bearer token") + .send( + deepmerge(courseScore, { + course: { charts: [{ md5: "a" }, { md5: "a" }, { md5: "a" }, { md5: "a" }] }, + }) + ); + + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Course Submission."); + + t.end(); + }); + + t.test("Should reject scores not on LN mode", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-KtchiIR-Version", "2.0.0") + .set("Authorization", "Bearer token") + .send( + deepmerge(courseScore, { + score: { + lntype: 1, + }, + }) + ); + + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "LN mode is the only supported mode for dans."); + + t.end(); + }); + + t.test("Should reject too few constraints", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-KtchiIR-Version", "2.0.0") + .set("Authorization", "Bearer token") + .send( + deepmerge( + courseScore, + { + course: { + constraint: ["GAUGE_LR2"], + }, + }, + { arrayMerge: (d, s) => s } + ) + ); + + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Constraints."); + + t.end(); + }); + + t.test("Should reject non-array constraints", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-KtchiIR-Version", "2.0.0") + .set("Authorization", "Bearer token") + .send( + deepmerge( + courseScore, + { + course: { + constraint: "foo", + }, + }, + { arrayMerge: (d, s) => s } + ) + ); + + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Constraints."); + + t.end(); + }); + + t.test("Should reject too many constraints", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-KtchiIR-Version", "2.0.0") + .set("Authorization", "Bearer token") + .send( + deepmerge( + courseScore, + { + course: { + constraint: ["a", "b", "c", "d"], + }, + }, + { arrayMerge: (d, s) => s } + ) + ); + + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Constraints."); + + t.end(); + }); + + t.test("Should reject invalid constraints", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-KtchiIR-Version", "2.0.0") + .set("Authorization", "Bearer token") + .send( + deepmerge( + courseScore, + { + course: { + constraint: ["foo", "bar"], + }, + }, + { arrayMerge: (d, s) => s } + ) + ); + + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Constraints."); + + t.end(); + }); + + t.test("Should reject invalid constraints for 3", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-KtchiIR-Version", "2.0.0") + .set("Authorization", "Bearer token") + .send( + deepmerge( + courseScore, + { + course: { + constraint: ["GAUGE_LR2", "MIRROR", "CHEAT_MODE"], + }, + }, + { arrayMerge: (d, s) => s } + ) + ); + + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Constraints."); + + t.end(); + }); + + t.end(); +}); + t.teardown(CloseAllConnections); diff --git a/server/src/server/router/ir/beatoraja/router.ts b/server/src/server/router/ir/beatoraja/router.ts index 771499dac..b5c05656e 100644 --- a/server/src/server/router/ir/beatoraja/router.ts +++ b/server/src/server/router/ir/beatoraja/router.ts @@ -141,7 +141,12 @@ router.post("/submit-score", async (req, res) => { router.post("/submit-course", async (req, res) => { const charts = req.body.course?.charts; - if (!charts || !Array.isArray(charts) || charts.length !== 4) { + if ( + !charts || + !Array.isArray(charts) || + charts.length !== 4 || + !charts.every((e) => e && typeof e === "object" && typeof e.md5 === "string") + ) { return res.status(400).json({ success: false, description: `Invalid Course Submission.`, @@ -166,8 +171,8 @@ router.post("/submit-course", async (req, res) => { // Constraints are a bit complicated. // We only want to accept dans with the following - // beatoraja constraints - ["MIRROR","LR2_GAUGE"] or - // ["MIRROR", "LR2_GAUGE", "LN"]. + // beatoraja constraints - ["MIRROR","GAUGE_LR2"] or + // ["MIRROR", "GAUGE_LR2", "LN"]. const constraint = req.body.course.constraint; if ( @@ -177,16 +182,16 @@ router.post("/submit-course", async (req, res) => { ) { return res.status(400).json({ success: false, - description: `Invalid constraints.`, + description: `Invalid Constraints.`, }); } // If there are two constraints, check that they are - // MIRROR and LR2_GAUGE. - if (!constraint.includes("MIRROR") || !constraint.includes("LR2_GAUGE")) { + // MIRROR and GAUGE_LR2. + if (!constraint.includes("MIRROR") || !constraint.includes("GAUGE_LR2")) { return res.status(400).json({ success: false, - description: `Invalid constraints.`, + description: `Invalid Constraints.`, }); } @@ -202,7 +207,7 @@ router.post("/submit-course", async (req, res) => { } // Combine the md5s into one string in their order. - const combinedMD5s = charts.map((e) => e.md5).join(); + const combinedMD5s = charts.map((e) => e.md5).join(""); const course = await db["bms-course-lookup"].findOne({ md5sums: combinedMD5s, @@ -225,10 +230,10 @@ router.post("/submit-course", async (req, res) => { course.value ); - if (result) { + if (result === false) { return res.status(200).json({ success: true, - description: "Successfully updated class.", + description: "Class not updated.", body: { set: course.set, value: course.value, @@ -238,7 +243,7 @@ router.post("/submit-course", async (req, res) => { return res.status(200).json({ success: true, - description: "Class not updated.", + description: "Successfully updated class.", body: { set: course.set, value: course.value, diff --git a/server/src/test-utils/mock-db/bms-course-lookup.json b/server/src/test-utils/mock-db/bms-course-lookup.json new file mode 100644 index 000000000..b80d8d03d --- /dev/null +++ b/server/src/test-utils/mock-db/bms-course-lookup.json @@ -0,0 +1,9 @@ +[ + { + "title": "GENOSIDE 2018 段位認定 Overjoy", + "md5sums": "cfad3baadce9e02c45021963453d7c9477d23be22b2370925c573d922276bce0188a99f74ab71804f2e360dcf484545cc46a81cb184f5a804c119930d6eba748", + "set": "genocideDan", + "playtype": "7K", + "value": 22 + } +] \ No newline at end of file