diff --git a/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.test.ts b/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.test.ts index 9cf6d8ef7..92dd75c1f 100644 --- a/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.test.ts +++ b/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.test.ts @@ -1,6 +1,7 @@ -import { ParseMyPageScraperRecordsCsv } from "./parser"; +import { ParseMyPageScraperRecordsCsv, ParseMyPageScraperPlayerCsv } from "./parser"; import CreateLogCtx from "lib/logger/logger"; import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error"; +import { WACCA_STAGEUPS } from "lib/constants/classes"; import t from "tap"; import { MockMulterFile } from "test-utils/mock-multer"; import { TestingWaccaMyPageScraperRecordsCSV } from "test-utils/test-data"; @@ -10,19 +11,153 @@ const logger = CreateLogCtx(__filename); t.test("#ParseMyPageScraperRecordsCSV", (t) => { t.test("Valid CSV", (t) => { - const file = MockMulterFile(TestingWaccaMyPageScraperRecordsCSV, "score.csv"); + // This example file is cg505's actual records dump. + const file = MockMulterFile(TestingWaccaMyPageScraperRecordsCSV, "records.csv"); const { iterable, game } = ParseMyPageScraperRecordsCsv(file, {}, logger); t.equal(game, "wacca"); const iterableData = iterable as Array; - // There are more than 280 lines in the file, but this includes song that are - // never played. The actual number of charts with (non-zero) PBs is 280. + // Each line in the file corresponds to a song, not a chart. So one line + // can have up to 4 PBs. There are more than 280 lines in the file, but + // this includes songs that are never played. The actual number of + // charts with (non-zero) PBs is 280. t.equal(iterableData.length, 280); t.end(); }); + t.test("Correctly parse CSV", (t) => { + const buffer = Buffer.from("music_id,music_title,music_artist,music_genre,music_levels,music_play_counts,music_scores,music_achieves\n3080,Avenue,aran,6,\"[3,7+,12+,0]\",\"[0,0,12]\",\"[0,0,996952]\",\"[0,0,3]\""); + + const file = MockMulterFile(buffer, "records.csv"); + + const { iterable, game } = ParseMyPageScraperRecordsCsv(file, {}, logger); + + t.equal(game, "wacca"); + + t.strictSame(iterable, [{ + songId: 3080, + songTitle: "Avenue", + diffIndex: 2, + level: "12+", + score: 996952, + lamp: 3, + }]); + + t.end(); + }); + + t.test("Malformed CSV", (t) => { + // Missing music_id in the record + const buffer = Buffer.from("music_id,music_title,music_artist,music_genre,music_levels,music_play_counts,music_scores,music_achieves\nAvenue,aran,6,\"[3,7+,12+,0]\",\"[0,0,12]\",\"[0,0,996952]\",\"[0,0,3]\""); + + const file = MockMulterFile(buffer, "records.csv"); + + t.throws( + () => ParseMyPageScraperRecordsCsv(file, {}, logger), + new ScoreImportFatalError(400, "Failed to parse CSV: Invalid Record Length: columns length is 8, got 7 on line 2") + ); + + t.end(); + }); + + t.test("CSV with wrong headers", (t) => { + // not_music_title instead of music_title + const buffer = Buffer.from("music_id,not_music_title,music_artist,music_genre,music_levels,music_play_counts,music_scores,music_achieves\n3080,Avenue,aran,6,\"[3,7+,12+,0]\",\"[0,0,12]\",\"[0,0,996952]\",\"[0,0,3]\""); + + const file = MockMulterFile(buffer, "records.csv"); + + t.throws( + () => ParseMyPageScraperRecordsCsv(file, {}, logger), + new ScoreImportFatalError(400, "Malformed CSV, invalid column(s) (music_title: undefined): Expected string.") + ); + + t.end(); + }); + + t.test("CSV with missing headers", (t) => { + // missing music_achieves + const buffer = Buffer.from("music_id,music_title,music_levels,music_scores\n3080,Avenue,\"[3,7+,12+,0]\",\"[0,0,996952]\""); + + const file = MockMulterFile(buffer, "records.csv"); + + t.throws( + () => ParseMyPageScraperRecordsCsv(file, {}, logger), + new ScoreImportFatalError(400, "Malformed CSV, invalid column(s) (music_achieves: undefined): Expected string.") + ); + + t.end(); + }); + + t.end(); +}); + +t.test("#ParseMyPageScraperPlayerCSV", (t) => { + t.test("Valid CSV", (t) => { + // This file is small so we just inline it. + const buffer = Buffer.from("player_name,player_level,player_rate,player_stage,player_play_count,player_play_count_versus,player_play_count_coop,player_total_rp_earned,player_total_rp_spent\ncg505,120,2704,\"[12,ステージXII,2]\",1274,57,0,2088515,531325"); + + + const file = MockMulterFile(buffer, "player.csv"); + + const { iterable, game, classHandler } = ParseMyPageScraperPlayerCsv(file, {}, logger); + + t.equal(game, "wacca"); + + t.strictSame(iterable, [], "Should not have any score data."); + + t.not(classHandler, null); + + // There's no good way to test that the classHandler got a valid + // MyPagePlayerStage, so we just call it to see. + t.strictSame(classHandler!("wacca", "Single", 0, {}, logger), {stageUp: WACCA_STAGEUPS.XII}); + + t.end(); + }); + + t.test("Malformed CSV", (t) => { + // Missing player_level in record + const buffer = Buffer.from("player_name,player_level,player_rate,player_stage,player_play_count,player_play_count_versus,player_play_count_coop,player_total_rp_earned,player_total_rp_spent\ncg505,2704,\"[12,ステージXII,2]\",1274,57,0,2088515,531325"); + + const file = MockMulterFile(buffer, "player.csv"); + + t.throws( + () => ParseMyPageScraperPlayerCsv(file, {}, logger), + new ScoreImportFatalError(400, "Failed to parse CSV: Invalid Record Length: columns length is 9, got 8 on line 2") + ); + + t.end(); + }); + + t.test("CSV missing player_stage", (t) => { + // This is the only value we actually care about. + const buffer = Buffer.from("player_name,player_level,player_rate,player_play_count,player_play_count_versus,player_play_count_coop,player_total_rp_earned,player_total_rp_spent\ncg505,120,2704,1274,57,0,2088515,531325"); + + const file = MockMulterFile(buffer, "player.csv"); + + t.throws( + () => ParseMyPageScraperPlayerCsv(file, {}, logger), + new ScoreImportFatalError(400, "Malformed CSV: no player_stage column.") + ); + + t.end(); + }); + + t.test("malformed player_stage array", (t) => { + // Missing the stage grade (third element) + const buffer = Buffer.from("player_stage\n\"[12,ステージXII]\""); + + const file = MockMulterFile(buffer, "player.csv"); + + t.throws( + () => ParseMyPageScraperPlayerCsv(file, {}, logger), + new ScoreImportFatalError(400, "Malformed player_stage entry.") + ); + + t.end(); + }); + t.end(); }); diff --git a/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.ts b/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.ts index 892d9e4f7..bdbc34cf7 100644 --- a/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.ts +++ b/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.ts @@ -65,7 +65,8 @@ export function ParseMyPageScraperRecordsCsv( const err = p(rawRecord, RECORDS_CSV_SCHEMA, {}, { allowExcessKeys: true }); if (err) { - throw new ScoreImportFatalError(400, `Malformed CSV, invalid columns: ${err.message}`); + // TODO: add some context so we don't get things like just "Expected string." + throw new ScoreImportFatalError(400, `Malformed CSV, invalid column(s) (${err.keychain}: ${err.userVal}): ${err.message}`); } } @@ -141,7 +142,7 @@ export function ParseMyPageScraperPlayerCsv( const stageArrayString = csvRecord.player_stage; if (stageArrayString === undefined) { - throw new ScoreImportFatalError(400, `Malformed CSV: no player_stage column.`); + throw new ScoreImportFatalError(400, "Malformed CSV: no player_stage column."); } // The first element is the full match, the next 3 elements are the groups. @@ -150,7 +151,7 @@ export function ParseMyPageScraperPlayerCsv( | null; if (matches === null) { - throw new ScoreImportFatalError(400, `Malformed player_stage entry.`); + throw new ScoreImportFatalError(400, "Malformed player_stage entry."); } const stage = {