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
This commit is contained in:
2026-08-29 20:44:05 +02:00
parent 9023af7e7e
commit 8e1a1c91e2
2 changed files with 62 additions and 3 deletions
+3
View File
@@ -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.
+59 -3
View File
@@ -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<Path>) -> Result<Self, rusqlite::Error> {
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, rusqlite::Error> {
Self::from_connection(Connection::open_in_memory()?)
Self::from_connection(Connection::open_in_memory()?, false)
}
fn from_connection(connection: Connection) -> Result<Self, rusqlite::Error> {
fn from_connection(
mut connection: Connection,
seed_defaults: bool,
) -> Result<Self, rusqlite::Error> {
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::<Vec<_>>();
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"));