diff --git a/tdkpin-rs/highscore-server/README.md b/tdkpin-rs/highscore-server/README.md index b4a1ca6..0737e34 100644 --- a/tdkpin-rs/highscore-server/README.md +++ b/tdkpin-rs/highscore-server/README.md @@ -11,6 +11,9 @@ POST /api/highscores The POST body is JSON with a 21-character maximum name and a `u32` score. The response is the canonical top-ten JSON array. +A newly created database starts with the original distributed demo table. An +existing database, including an existing empty database, is left unchanged. + Submissions are anonymous and intentionally trust the browser's score. Add rate limiting, moderation, or server-side run verification if the table needs to resist forged scores. diff --git a/tdkpin-rs/highscore-server/src/lib.rs b/tdkpin-rs/highscore-server/src/lib.rs index 9338861..7cc3248 100644 --- a/tdkpin-rs/highscore-server/src/lib.rs +++ b/tdkpin-rs/highscore-server/src/lib.rs @@ -19,6 +19,18 @@ const MAX_HIGH_SCORES: usize = 10; const MAX_NAME_CHARS: usize = 21; const MAX_SUBMISSION_BODY_BYTES: usize = 1024; const MAX_CONCURRENT_DATABASE_OPERATIONS: usize = 1; +const DEFAULT_HIGH_SCORES: &[(&str, u32)] = &[ + ("Paul", 6_537_392), + ("Paul Schulze", 2_979_000), + ("Kalle", 2_393_464), + ("Paul Schulze", 2_328_000), + ("Martina Sommer", 2_326_000), + ("Martina Sommer", 1_093_000), + ("Sommer Martina", 1_027_000), + ("No Name", 1_000_000), + ("TDK Pinball Player", 923_000), + ("Martina Sommer", 905_000), +]; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct HighScore { @@ -49,7 +61,9 @@ impl HighScoreStore { /// Returns the SQLite error raised while opening or initializing the /// database. pub fn open(path: impl AsRef) -> Result { - Self::from_connection(Connection::open(path)?) + let path = path.as_ref(); + let seed_defaults = !path.exists(); + Self::from_connection(Connection::open(path)?, seed_defaults) } /// Create an in-memory high-score store for tests or short-lived runs. @@ -58,10 +72,13 @@ impl HighScoreStore { /// /// Returns the SQLite error raised while initializing the database. pub fn open_in_memory() -> Result { - Self::from_connection(Connection::open_in_memory()?) + Self::from_connection(Connection::open_in_memory()?, false) } - fn from_connection(connection: Connection) -> Result { + fn from_connection( + mut connection: Connection, + seed_defaults: bool, + ) -> Result { connection.execute_batch( "CREATE TABLE IF NOT EXISTS high_scores ( id INTEGER PRIMARY KEY, @@ -71,6 +88,16 @@ impl HighScoreStore { CREATE INDEX IF NOT EXISTS high_scores_order ON high_scores (score DESC, id ASC);", )?; + if seed_defaults { + let transaction = connection.transaction()?; + for &(name, score) in DEFAULT_HIGH_SCORES { + transaction.execute( + "INSERT INTO high_scores (name, score) VALUES (?1, ?2)", + params![name, i64::from(score)], + )?; + } + transaction.commit()?; + } Ok(Self(Arc::new(Mutex::new(connection)))) } @@ -359,6 +386,7 @@ mod tests { async fn api_persists_and_keeps_the_top_ten() { let directory = tempdir().expect("temporary directory should exist"); let database = directory.path().join("highscores.sqlite3"); + std::fs::File::create(&database).expect("database file should exist"); let store = HighScoreStore::open(&database).expect("database should open"); let app = router(store); @@ -378,6 +406,34 @@ mod tests { assert_eq!(list(&reopened).await, scores); } + #[test] + fn new_database_starts_with_the_original_demo_scores() { + let directory = tempdir().expect("temporary directory should exist"); + let database = directory.path().join("highscores.sqlite3"); + let store = HighScoreStore::open(&database).expect("database should open"); + + let scores = store.list().expect("scores should list"); + let expected = DEFAULT_HIGH_SCORES + .iter() + .map(|&(name, score)| HighScore { + name: name.to_owned(), + score, + }) + .collect::>(); + assert_eq!(scores, expected); + } + + #[test] + fn existing_database_is_not_seeded() { + let directory = tempdir().expect("temporary directory should exist"); + let database = directory.path().join("highscores.sqlite3"); + std::fs::File::create(&database).expect("database file should exist"); + + let store = HighScoreStore::open(&database).expect("database should open"); + + assert!(store.list().expect("scores should list").is_empty()); + } + #[tokio::test] async fn api_rejects_invalid_names() { let app = router(HighScoreStore::open_in_memory().expect("database should open"));