Implements account management with password hashing (pbkdf2_hmac with SHA256) and constant-time comparison. Includes player state serialization for position and inventory persistence across sessions.
179 lines
4.8 KiB
Python
179 lines
4.8 KiB
Python
"""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,
|
|
)
|
|
|
|
|
|
@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
|