"""Tests for the store (persistence) module.""" import os import tempfile from pathlib import Path import pytest from mudlib.player import Player from mudlib.store import ( account_exists, authenticate, create_account, init_db, load_player_data, save_player, ) from mudlib.zone import Zone @pytest.fixture def temp_db(): """Create a temporary database for testing.""" with tempfile.NamedTemporaryFile(delete=False, suffix=".db") as f: db_path = f.name init_db(db_path) yield db_path # Cleanup os.unlink(db_path) def test_init_db_creates_file(temp_db): """init_db creates the database file.""" assert Path(temp_db).exists() def test_create_account_success(temp_db): """create_account creates a new account.""" assert create_account("Alice", "password123") assert account_exists("Alice") def test_create_account_case_insensitive(temp_db): """Account names are case-insensitive.""" create_account("Bob", "password123") assert account_exists("bob") assert account_exists("BOB") assert account_exists("Bob") def test_create_account_duplicate_fails(temp_db): """create_account fails if name is already taken.""" assert create_account("Charlie", "password123") assert not create_account("Charlie", "different_password") assert not create_account("charlie", "different_password") # case insensitive def test_authenticate_success(temp_db): """authenticate returns True for correct password.""" create_account("Dave", "correct_password") assert authenticate("Dave", "correct_password") def test_authenticate_case_insensitive_name(temp_db): """authenticate works with case-insensitive names.""" create_account("Eve", "password123") assert authenticate("eve", "password123") assert authenticate("EVE", "password123") def test_authenticate_wrong_password(temp_db): """authenticate returns False for wrong password.""" create_account("Frank", "correct_password") assert not authenticate("Frank", "wrong_password") def test_authenticate_nonexistent_account(temp_db): """authenticate returns False for nonexistent account.""" assert not authenticate("Ghost", "any_password") def test_save_and_load_player_data(temp_db): """save_player and load_player_data persist player state.""" # Create account first create_account("Grace", "password123") # Create a player with non-default values player = Player( name="Grace", x=42, y=17, pl=85.5, stamina=60.0, max_stamina=120.0, flying=True, ) # Save and load save_player(player) data = load_player_data("Grace") assert data is not None assert data["x"] == 42 assert data["y"] == 17 assert data["pl"] == 85.5 assert data["stamina"] == 60.0 assert data["max_stamina"] == 120.0 assert data["flying"] is True def test_load_player_data_case_insensitive(temp_db): """load_player_data works with case-insensitive names.""" create_account("Henry", "password123") player = Player(name="Henry", x=10, y=20) save_player(player) data_lower = load_player_data("henry") data_upper = load_player_data("HENRY") assert data_lower is not None assert data_upper is not None assert data_lower["x"] == 10 assert data_upper["x"] == 10 def test_load_player_data_nonexistent(temp_db): """load_player_data returns None for nonexistent account.""" assert load_player_data("Nobody") is None def test_save_player_updates_existing(temp_db): """save_player updates existing player data.""" create_account("Iris", "password123") # First save player = Player(name="Iris", x=10, y=20, pl=100.0) save_player(player) # Update and save again player.x = 50 player.y = 60 player.pl = 75.0 save_player(player) # Load and verify data = load_player_data("Iris") assert data is not None assert data["x"] == 50 assert data["y"] == 60 assert data["pl"] == 75.0 def test_default_values(temp_db): """New accounts have default values.""" create_account("Jack", "password123") data = load_player_data("Jack") assert data is not None assert data["x"] == 0 assert data["y"] == 0 assert data["pl"] == 100.0 assert data["stamina"] == 100.0 assert data["max_stamina"] == 100.0 assert data["flying"] is False def test_password_hashing_different_salts(temp_db): """Different accounts with same password have different hashes.""" create_account("Kate", "same_password") create_account("Leo", "same_password") # Both should authenticate assert authenticate("Kate", "same_password") assert authenticate("Leo", "same_password") # This just verifies the API works correctly - we can't easily check # the hashes are different without exposing internal details def test_save_and_load_zone_name(temp_db): """save_player and load_player_data persist zone_name.""" create_account("Maria", "password123") # Create a zone and player zone = Zone(name="testzone", width=100, height=100) player = Player( name="Maria", location=zone, x=10, y=20, ) # Save and load save_player(player) data = load_player_data("Maria") assert data is not None assert data["zone_name"] == "testzone" def test_default_zone_name(temp_db): """New accounts have zone_name default to 'overworld'.""" create_account("Noah", "password123") data = load_player_data("Noah") assert data is not None assert data["zone_name"] == "overworld" def test_zone_name_migration(temp_db): """Existing DB without zone_name column still works.""" # Create account, which will create default schema create_account("Olivia", "password123") # Simulate old DB by manually removing the zone_name column import sqlite3 conn = sqlite3.connect(temp_db) cursor = conn.cursor() # Check if column exists and remove it cursor.execute("PRAGMA table_info(accounts)") columns = [row[1] for row in cursor.fetchall()] if "zone_name" in columns: # SQLite doesn't support DROP COLUMN directly, so recreate table cursor.execute(""" CREATE TABLE accounts_backup AS SELECT name, password_hash, salt, x, y, pl, stamina, max_stamina, flying, created_at, last_login FROM accounts """) cursor.execute("DROP TABLE accounts") cursor.execute(""" CREATE TABLE accounts ( name TEXT PRIMARY KEY COLLATE NOCASE, password_hash TEXT NOT NULL, salt TEXT NOT NULL, x INTEGER NOT NULL DEFAULT 0, y INTEGER NOT NULL DEFAULT 0, pl REAL NOT NULL DEFAULT 100.0, stamina REAL NOT NULL DEFAULT 100.0, max_stamina REAL NOT NULL DEFAULT 100.0, flying INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), last_login TEXT ) """) cursor.execute(""" INSERT INTO accounts SELECT * FROM accounts_backup """) cursor.execute("DROP TABLE accounts_backup") conn.commit() conn.close() # Now try to load player data - should handle missing column gracefully data = load_player_data("Olivia") assert data is not None assert data["zone_name"] == "overworld" # Should default