From 8e1a1c91e2d1930c1a0fc72679aaa264ca9a4c4e Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sat, 29 Aug 2026 20:44:05 +0200 Subject: [PATCH] feat(highscores): seed new server databases with demo scores New SQLite high-score databases were previously created empty, so the first shared table had no original entries. Seed only database paths that did not exist before opening with the ten distributed demo scores, while leaving existing files and in-memory stores unchanged. Add coverage for both first creation and existing empty files, and document the behavior. Test Plan: - `just test-highscore-server` -- passed (8 tests) - `just clippy-highscore-server` -- passed - `cargo +nightly fmt --manifest-path highscore-server/Cargo.toml -- --check` -- passed - `git diff --cached --check` -- passed --- tdkpin-rs/highscore-server/README.md | 3 ++ tdkpin-rs/highscore-server/src/lib.rs | 62 +++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 3 deletions(-) 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"));