Dragon-Knight/internal/users/users_test.go

671 lines
18 KiB
Go

package users
import (
"os"
"testing"
"time"
"dk/internal/database"
)
func setupTestDB(t *testing.T) *database.DB {
testDB := "test_users.db"
t.Cleanup(func() {
os.Remove(testDB)
})
db, err := database.Open(testDB)
if err != nil {
t.Fatalf("Failed to open test database: %v", err)
}
// Create users table
createTable := `CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
password TEXT NOT NULL,
email TEXT NOT NULL,
verified INTEGER NOT NULL DEFAULT 0,
token TEXT NOT NULL DEFAULT '',
registered INTEGER NOT NULL DEFAULT (unixepoch()),
last_online INTEGER NOT NULL DEFAULT (unixepoch()),
auth INTEGER NOT NULL DEFAULT 0,
x INTEGER NOT NULL DEFAULT 0,
y INTEGER NOT NULL DEFAULT 0,
class_id INTEGER NOT NULL DEFAULT 0,
currently TEXT NOT NULL DEFAULT 'In Town',
fighting INTEGER NOT NULL DEFAULT 0,
monster_id INTEGER NOT NULL DEFAULT 0,
monster_hp INTEGER NOT NULL DEFAULT 0,
monster_sleep INTEGER NOT NULL DEFAULT 0,
monster_immune INTEGER NOT NULL DEFAULT 0,
uber_damage INTEGER NOT NULL DEFAULT 0,
uber_defense INTEGER NOT NULL DEFAULT 0,
hp INTEGER NOT NULL DEFAULT 15,
mp INTEGER NOT NULL DEFAULT 0,
tp INTEGER NOT NULL DEFAULT 10,
max_hp INTEGER NOT NULL DEFAULT 15,
max_mp INTEGER NOT NULL DEFAULT 0,
max_tp INTEGER NOT NULL DEFAULT 10,
level INTEGER NOT NULL DEFAULT 1,
gold INTEGER NOT NULL DEFAULT 100,
exp INTEGER NOT NULL DEFAULT 0,
gold_bonus INTEGER NOT NULL DEFAULT 0,
exp_bonus INTEGER NOT NULL DEFAULT 0,
strength INTEGER NOT NULL DEFAULT 5,
dexterity INTEGER NOT NULL DEFAULT 5,
attack INTEGER NOT NULL DEFAULT 5,
defense INTEGER NOT NULL DEFAULT 5,
weapon_id INTEGER NOT NULL DEFAULT 0,
armor_id INTEGER NOT NULL DEFAULT 0,
shield_id INTEGER NOT NULL DEFAULT 0,
slot_1_id INTEGER NOT NULL DEFAULT 0,
slot_2_id INTEGER NOT NULL DEFAULT 0,
slot_3_id INTEGER NOT NULL DEFAULT 0,
weapon_name TEXT NOT NULL DEFAULT '',
armor_name TEXT NOT NULL DEFAULT '',
shield_name TEXT NOT NULL DEFAULT '',
slot_1_name TEXT NOT NULL DEFAULT '',
slot_2_name TEXT NOT NULL DEFAULT '',
slot_3_name TEXT NOT NULL DEFAULT '',
drop_code INTEGER NOT NULL DEFAULT 0,
spells TEXT NOT NULL DEFAULT '',
towns TEXT NOT NULL DEFAULT ''
)`
if err := db.Exec(createTable); err != nil {
t.Fatalf("Failed to create users table: %v", err)
}
// Insert test data with specific timestamps
now := time.Now().Unix()
testUsers := `INSERT INTO users (username, password, email, verified, token, registered, last_online, auth,
x, y, class_id, level, gold, exp, hp, mp, tp, max_hp, max_mp, max_tp,
strength, dexterity, attack, defense, spells, towns) VALUES
('alice', 'hashed_pass_1', 'alice@example.com', 1, '', ?, ?, 0, 10, 20, 1, 5, 500, 1250, 25, 15, 12, 25, 15, 12, 8, 7, 10, 8, '1,2,5', '1,2'),
('bob', 'hashed_pass_2', 'bob@example.com', 1, '', ?, ?, 2, -5, 15, 2, 3, 300, 750, 20, 8, 10, 20, 8, 10, 6, 8, 8, 9, '3,4', '1'),
('charlie', 'hashed_pass_3', 'charlie@example.com', 0, 'verify_token_123', ?, ?, 4, 0, 0, 3, 1, 100, 0, 15, 0, 10, 15, 0, 10, 5, 5, 5, 5, '', ''),
('diana', 'hashed_pass_4', 'diana@example.com', 1, '', ?, ?, 0, 25, -10, 1, 8, 1200, 3500, 35, 25, 15, 35, 25, 15, 12, 10, 15, 12, '1,2,3,6,7', '1,2,3,4')`
timestamps := []any{
now - 86400*7, now - 3600*2, // alice: registered 1 week ago, last online 2 hours ago
now - 86400*5, now - 86400*1, // bob: registered 5 days ago, last online 1 day ago
now - 86400*1, now - 86400*1, // charlie: registered 1 day ago, last online 1 day ago
now - 86400*30, now - 3600*1, // diana: registered 1 month ago, last online 1 hour ago
}
if err := db.Exec(testUsers, timestamps...); err != nil {
t.Fatalf("Failed to insert test users: %v", err)
}
return db
}
func TestFind(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test finding existing user
user, err := Find(db, 1)
if err != nil {
t.Fatalf("Failed to find user: %v", err)
}
if user.ID != 1 {
t.Errorf("Expected ID 1, got %d", user.ID)
}
if user.Username != "alice" {
t.Errorf("Expected username 'alice', got '%s'", user.Username)
}
if user.Email != "alice@example.com" {
t.Errorf("Expected email 'alice@example.com', got '%s'", user.Email)
}
if user.Verified != 1 {
t.Errorf("Expected verified 1, got %d", user.Verified)
}
if user.Auth != 0 {
t.Errorf("Expected auth 0, got %d", user.Auth)
}
if user.Level != 5 {
t.Errorf("Expected level 5, got %d", user.Level)
}
// Test finding non-existent user
_, err = Find(db, 999)
if err == nil {
t.Error("Expected error when finding non-existent user")
}
}
func TestAll(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
users, err := All(db)
if err != nil {
t.Fatalf("Failed to get all users: %v", err)
}
if len(users) != 4 {
t.Errorf("Expected 4 users, got %d", len(users))
}
// Check ordering (by registered DESC)
if len(users) >= 2 {
if users[0].Registered < users[1].Registered {
t.Error("Expected users to be ordered by registration date (newest first)")
}
}
}
func TestByUsername(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test finding existing user by username
user, err := ByUsername(db, "alice")
if err != nil {
t.Fatalf("Failed to find user by username: %v", err)
}
if user.Username != "alice" {
t.Errorf("Expected username 'alice', got '%s'", user.Username)
}
if user.ID != 1 {
t.Errorf("Expected ID 1, got %d", user.ID)
}
// Test case insensitive search
userUpper, err := ByUsername(db, "ALICE")
if err != nil {
t.Fatalf("Failed to find user by uppercase username: %v", err)
}
if userUpper.ID != user.ID {
t.Error("Expected case insensitive search to return same user")
}
// Test non-existent user
_, err = ByUsername(db, "nonexistent")
if err == nil {
t.Error("Expected error when finding non-existent user by username")
}
}
func TestByEmail(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test finding existing user by email
user, err := ByEmail(db, "bob@example.com")
if err != nil {
t.Fatalf("Failed to find user by email: %v", err)
}
if user.Email != "bob@example.com" {
t.Errorf("Expected email 'bob@example.com', got '%s'", user.Email)
}
if user.Username != "bob" {
t.Errorf("Expected username 'bob', got '%s'", user.Username)
}
// Test non-existent email
_, err = ByEmail(db, "nonexistent@example.com")
if err == nil {
t.Error("Expected error when finding non-existent user by email")
}
}
func TestByLevel(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test users at level 1
level1Users, err := ByLevel(db, 1)
if err != nil {
t.Fatalf("Failed to get users by level: %v", err)
}
expectedCount := 1 // Charlie is level 1
if len(level1Users) != expectedCount {
t.Errorf("Expected %d users at level 1, got %d", expectedCount, len(level1Users))
}
// Verify all users are level 1
for _, user := range level1Users {
if user.Level != 1 {
t.Errorf("Expected level 1, got %d for user %s", user.Level, user.Username)
}
}
// Test level with no users
noUsers, err := ByLevel(db, 99)
if err != nil {
t.Fatalf("Failed to query non-existent level: %v", err)
}
if len(noUsers) != 0 {
t.Errorf("Expected 0 users at level 99, got %d", len(noUsers))
}
}
func TestOnline(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test users online within the last 6 hours
onlineUsers, err := Online(db, 6*time.Hour)
if err != nil {
t.Fatalf("Failed to get online users: %v", err)
}
// Alice (2 hours ago) and Diana (1 hour ago) should be included
expectedCount := 2
if len(onlineUsers) != expectedCount {
t.Errorf("Expected %d users online within 6 hours, got %d", expectedCount, len(onlineUsers))
}
// Check ordering (by last_online DESC)
if len(onlineUsers) >= 2 {
if onlineUsers[0].LastOnline < onlineUsers[1].LastOnline {
t.Error("Expected online users to be ordered by last online time")
}
}
// Test narrow time window
recentUsers, err := Online(db, 30*time.Minute)
if err != nil {
t.Fatalf("Failed to get recently online users: %v", err)
}
// No users should be online within the last 30 minutes
if len(recentUsers) != 0 {
t.Errorf("Expected 0 users online within 30 minutes, got %d", len(recentUsers))
}
}
func TestBuilder(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Create new user using builder
testTime := time.Now()
user, err := NewBuilder(db).
WithUsername("testuser").
WithPassword("hashed_password").
WithEmail("test@example.com").
WithVerified(true).
WithAuth(2).
WithClassID(2).
WithPosition(50, -25).
WithLevel(3).
WithGold(250).
WithStats(7, 6, 8, 7).
WithHP(20, 20).
WithMP(10, 10).
WithTP(12, 12).
WithCurrently("Exploring").
WithRegisteredTime(testTime).
WithSpells([]string{"1", "3", "5"}).
WithTowns([]string{"1", "2"}).
Create()
if err != nil {
t.Fatalf("Failed to create user with builder: %v", err)
}
if user.ID == 0 {
t.Error("Expected non-zero ID after creation")
}
if user.Username != "testuser" {
t.Errorf("Expected username 'testuser', got '%s'", user.Username)
}
if user.Email != "test@example.com" {
t.Errorf("Expected email 'test@example.com', got '%s'", user.Email)
}
if user.Verified != 1 {
t.Errorf("Expected verified 1, got %d", user.Verified)
}
if user.Auth != 2 {
t.Errorf("Expected auth 2, got %d", user.Auth)
}
if user.ClassID != 2 {
t.Errorf("Expected class_id 2, got %d", user.ClassID)
}
if user.X != 50 || user.Y != -25 {
t.Errorf("Expected position (50, -25), got (%d, %d)", user.X, user.Y)
}
if user.Level != 3 {
t.Errorf("Expected level 3, got %d", user.Level)
}
if user.Gold != 250 {
t.Errorf("Expected gold 250, got %d", user.Gold)
}
if user.Registered != testTime.Unix() {
t.Errorf("Expected registered time %d, got %d", testTime.Unix(), user.Registered)
}
// Verify it was saved to database
foundUser, err := Find(db, user.ID)
if err != nil {
t.Fatalf("Failed to find created user: %v", err)
}
if foundUser.Username != "testuser" {
t.Errorf("Created user not found in database")
}
// Test builder with defaults
defaultUser, err := NewBuilder(db).
WithUsername("defaultuser").
WithPassword("password").
WithEmail("default@example.com").
Create()
if err != nil {
t.Fatalf("Failed to create user with defaults: %v", err)
}
// Should have default values
if defaultUser.Level != 1 {
t.Errorf("Expected default level 1, got %d", defaultUser.Level)
}
if defaultUser.Gold != 100 {
t.Errorf("Expected default gold 100, got %d", defaultUser.Gold)
}
if defaultUser.HP != 15 {
t.Errorf("Expected default HP 15, got %d", defaultUser.HP)
}
// Test convenience methods
adminUser, err := NewBuilder(db).
WithUsername("admin").
WithPassword("admin_pass").
WithEmail("admin@example.com").
AsAdmin().
Create()
if err != nil {
t.Fatalf("Failed to create admin user: %v", err)
}
if adminUser.Auth != 4 {
t.Errorf("Expected admin auth 4, got %d", adminUser.Auth)
}
moderatorUser, err := NewBuilder(db).
WithUsername("mod").
WithPassword("mod_pass").
WithEmail("mod@example.com").
AsModerator().
Create()
if err != nil {
t.Fatalf("Failed to create moderator user: %v", err)
}
if moderatorUser.Auth != 2 {
t.Errorf("Expected moderator auth 2, got %d", moderatorUser.Auth)
}
}
func TestSave(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
user, err := Find(db, 1)
if err != nil {
t.Fatalf("Failed to find user: %v", err)
}
// Modify user
user.Username = "alice_updated"
user.Email = "alice_updated@example.com"
user.Level = 10
user.Gold = 1000
user.UpdateLastOnline()
// Save changes
err = user.Save()
if err != nil {
t.Fatalf("Failed to save user: %v", err)
}
// Verify changes were saved
updatedUser, err := Find(db, 1)
if err != nil {
t.Fatalf("Failed to find updated user: %v", err)
}
if updatedUser.Username != "alice_updated" {
t.Errorf("Expected updated username 'alice_updated', got '%s'", updatedUser.Username)
}
if updatedUser.Email != "alice_updated@example.com" {
t.Errorf("Expected updated email, got '%s'", updatedUser.Email)
}
if updatedUser.Level != 10 {
t.Errorf("Expected updated level 10, got %d", updatedUser.Level)
}
if updatedUser.Gold != 1000 {
t.Errorf("Expected updated gold 1000, got %d", updatedUser.Gold)
}
}
func TestDelete(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
user, err := Find(db, 1)
if err != nil {
t.Fatalf("Failed to find user: %v", err)
}
// Delete user
err = user.Delete()
if err != nil {
t.Fatalf("Failed to delete user: %v", err)
}
// Verify user was deleted
_, err = Find(db, 1)
if err == nil {
t.Error("Expected error when finding deleted user")
}
}
func TestUserMethods(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
alice, _ := Find(db, 1) // verified, auth 0
bob, _ := Find(db, 2) // verified, auth 2 (moderator)
charlie, _ := Find(db, 3) // unverified, auth 4 (admin)
// Test time methods
registeredTime := alice.RegisteredTime()
if registeredTime.IsZero() {
t.Error("Expected non-zero registered time")
}
lastOnlineTime := alice.LastOnlineTime()
if lastOnlineTime.IsZero() {
t.Error("Expected non-zero last online time")
}
// Test UpdateLastOnline
originalLastOnline := alice.LastOnline
alice.UpdateLastOnline()
if alice.LastOnline <= originalLastOnline {
t.Error("Expected last online to be updated to current time")
}
// Test verification status
if !alice.IsVerified() {
t.Error("Expected alice to be verified")
}
if charlie.IsVerified() {
t.Error("Expected charlie not to be verified")
}
// Test authorization levels
if alice.IsAdmin() {
t.Error("Expected alice not to be admin")
}
if alice.IsModerator() {
t.Error("Expected alice not to be moderator")
}
if !bob.IsModerator() {
t.Error("Expected bob to be moderator")
}
if !charlie.IsAdmin() {
t.Error("Expected charlie to be admin")
}
// Test combat status
if alice.IsFighting() {
t.Error("Expected alice not to be fighting")
}
if !alice.IsAlive() {
t.Error("Expected alice to be alive")
}
// Test position
x, y := alice.GetPosition()
if x != 10 || y != 20 {
t.Errorf("Expected position (10, 20), got (%d, %d)", x, y)
}
alice.SetPosition(30, 40)
x, y = alice.GetPosition()
if x != 30 || y != 40 {
t.Errorf("Expected updated position (30, 40), got (%d, %d)", x, y)
}
}
func TestSpellMethods(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
alice, _ := Find(db, 1) // spells: "1,2,5"
// Test GetSpellIDs
spells := alice.GetSpellIDs()
expectedSpells := []string{"1", "2", "5"}
if len(spells) != len(expectedSpells) {
t.Errorf("Expected %d spells, got %d", len(expectedSpells), len(spells))
}
for i, expected := range expectedSpells {
if i < len(spells) && spells[i] != expected {
t.Errorf("Expected spell '%s' at position %d, got '%s'", expected, i, spells[i])
}
}
// Test HasSpell
if !alice.HasSpell("1") {
t.Error("Expected alice to have spell '1'")
}
if !alice.HasSpell("2") {
t.Error("Expected alice to have spell '2'")
}
if alice.HasSpell("3") {
t.Error("Expected alice not to have spell '3'")
}
// Test SetSpellIDs
newSpells := []string{"3", "4", "6", "7"}
alice.SetSpellIDs(newSpells)
if alice.Spells != "3,4,6,7" {
t.Errorf("Expected spells '3,4,6,7', got '%s'", alice.Spells)
}
// Test with empty spells
charlie, _ := Find(db, 3) // empty spells
emptySpells := charlie.GetSpellIDs()
if len(emptySpells) != 0 {
t.Errorf("Expected 0 spells for empty list, got %d", len(emptySpells))
}
}
func TestTownMethods(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
alice, _ := Find(db, 1) // towns: "1,2"
// Test GetTownIDs
towns := alice.GetTownIDs()
expectedTowns := []string{"1", "2"}
if len(towns) != len(expectedTowns) {
t.Errorf("Expected %d towns, got %d", len(expectedTowns), len(towns))
}
for i, expected := range expectedTowns {
if i < len(towns) && towns[i] != expected {
t.Errorf("Expected town '%s' at position %d, got '%s'", expected, i, towns[i])
}
}
// Test HasVisitedTown
if !alice.HasVisitedTown("1") {
t.Error("Expected alice to have visited town '1'")
}
if !alice.HasVisitedTown("2") {
t.Error("Expected alice to have visited town '2'")
}
if alice.HasVisitedTown("3") {
t.Error("Expected alice not to have visited town '3'")
}
// Test SetTownIDs
newTowns := []string{"1", "2", "3", "4"}
alice.SetTownIDs(newTowns)
if alice.Towns != "1,2,3,4" {
t.Errorf("Expected towns '1,2,3,4', got '%s'", alice.Towns)
}
// Test with empty towns
charlie, _ := Find(db, 3) // empty towns
emptyTowns := charlie.GetTownIDs()
if len(emptyTowns) != 0 {
t.Errorf("Expected 0 towns for empty list, got %d", len(emptyTowns))
}
}
func TestGetEquipmentAndStats(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
alice, _ := Find(db, 1)
// Test GetEquipment
equipment := alice.GetEquipment()
if equipment == nil {
t.Error("Expected non-nil equipment map")
}
weapon, ok := equipment["weapon"].(map[string]any)
if !ok {
t.Error("Expected weapon to be a map")
}
if weapon["id"].(int) != alice.WeaponID {
t.Errorf("Expected weapon ID %d, got %v", alice.WeaponID, weapon["id"])
}
// Test GetStats
stats := alice.GetStats()
if stats == nil {
t.Error("Expected non-nil stats map")
}
if stats["level"] != alice.Level {
t.Errorf("Expected level %d, got %d", alice.Level, stats["level"])
}
if stats["hp"] != alice.HP {
t.Errorf("Expected HP %d, got %d", alice.HP, stats["hp"])
}
if stats["strength"] != alice.Strength {
t.Errorf("Expected strength %d, got %d", alice.Strength, stats["strength"])
}
}