// SPDX-License-Identifier: AGPL-3.0-or-later package store import ( "database/sql" "testing" _ "modernc.org/sqlite" "kode.naiv.no/olemd/favoritter/internal/database" ) func testDB(t *testing.T) *sql.DB { t.Helper() db, err := database.Open(":memory:") if err != nil { t.Fatalf("open test db: %v", err) } if err := database.Migrate(db); err != nil { t.Fatalf("migrate test db: %v", err) } t.Cleanup(func() { db.Close() }) return db } func TestCreateAndAuthenticate(t *testing.T) { db := testDB(t) users := NewUserStore(db) // Use fast Argon2 parameters for tests. Argon2Memory = 1024 Argon2Time = 1 defer func() { Argon2Memory = 65536 Argon2Time = 3 }() // Create a user. user, err := users.Create("testuser", "password123", "user") if err != nil { t.Fatalf("create user: %v", err) } if user.Username != "testuser" { t.Errorf("username = %q, want %q", user.Username, "testuser") } if user.Role != "user" { t.Errorf("role = %q, want %q", user.Role, "user") } // Authenticate with correct password. authed, err := users.Authenticate("testuser", "password123") if err != nil { t.Fatalf("authenticate: %v", err) } if authed.ID != user.ID { t.Errorf("authenticated user ID = %d, want %d", authed.ID, user.ID) } // Authenticate with wrong password. _, err = users.Authenticate("testuser", "wrongpassword") if err != ErrInvalidCredentials { t.Errorf("wrong password error = %v, want ErrInvalidCredentials", err) } // Authenticate with non-existent user. _, err = users.Authenticate("nouser", "password123") if err != ErrInvalidCredentials { t.Errorf("non-existent user error = %v, want ErrInvalidCredentials", err) } } func TestCreateDuplicate(t *testing.T) { db := testDB(t) users := NewUserStore(db) Argon2Memory = 1024 Argon2Time = 1 defer func() { Argon2Memory = 65536 Argon2Time = 3 }() _, err := users.Create("testuser", "password123", "user") if err != nil { t.Fatalf("create user: %v", err) } _, err = users.Create("testuser", "password456", "user") if err != ErrUserExists { t.Errorf("duplicate error = %v, want ErrUserExists", err) } // Case-insensitive duplicate. _, err = users.Create("TestUser", "password456", "user") if err != ErrUserExists { t.Errorf("case-insensitive duplicate error = %v, want ErrUserExists", err) } } func TestUpdatePassword(t *testing.T) { db := testDB(t) users := NewUserStore(db) Argon2Memory = 1024 Argon2Time = 1 defer func() { Argon2Memory = 65536 Argon2Time = 3 }() user, err := users.CreateWithReset("admin", "temppass", "admin") if err != nil { t.Fatalf("create user: %v", err) } if !user.MustResetPassword { t.Error("expected must_reset_password to be true") } err = users.UpdatePassword(user.ID, "newpassword123") if err != nil { t.Fatalf("update password: %v", err) } // Verify old password no longer works. _, err = users.Authenticate("admin", "temppass") if err != ErrInvalidCredentials { t.Error("old password should not work after reset") } // Verify new password works and reset flag is cleared. updated, err := users.Authenticate("admin", "newpassword123") if err != nil { t.Fatalf("authenticate with new password: %v", err) } if updated.MustResetPassword { t.Error("must_reset_password should be false after update") } } func TestEnsureAdmin(t *testing.T) { db := testDB(t) users := NewUserStore(db) Argon2Memory = 1024 Argon2Time = 1 defer func() { Argon2Memory = 65536 Argon2Time = 3 }() // First call creates the admin. err := users.EnsureAdmin("admin", "adminpass") if err != nil { t.Fatalf("ensure admin: %v", err) } admin, err := users.GetByUsername("admin") if err != nil { t.Fatalf("get admin: %v", err) } if !admin.IsAdmin() { t.Error("expected admin role") } // Second call is a no-op. err = users.EnsureAdmin("admin", "adminpass") if err != nil { t.Fatalf("ensure admin (second call): %v", err) } count, _ := users.Count() if count != 1 { t.Errorf("user count = %d, want 1", count) } } func TestDisabledUser(t *testing.T) { db := testDB(t) users := NewUserStore(db) Argon2Memory = 1024 Argon2Time = 1 defer func() { Argon2Memory = 65536 Argon2Time = 3 }() user, err := users.Create("testuser", "password123", "user") if err != nil { t.Fatalf("create user: %v", err) } // Disable the user. err = users.SetDisabled(user.ID, true) if err != nil { t.Fatalf("disable user: %v", err) } // Authentication should fail. _, err = users.Authenticate("testuser", "password123") if err != ErrUserDisabled { t.Errorf("disabled user error = %v, want ErrUserDisabled", err) } } func TestSetRole(t *testing.T) { db := testDB(t) users := NewUserStore(db) Argon2Memory = 1024 Argon2Time = 1 defer func() { Argon2Memory = 65536; Argon2Time = 3 }() user, _ := users.Create("testuser", "password123", "user") if user.Role != "user" { t.Fatalf("initial role = %q", user.Role) } // Promote to admin. err := users.SetRole(user.ID, "admin") if err != nil { t.Fatalf("set role: %v", err) } updated, _ := users.GetByID(user.ID) if updated.Role != "admin" { t.Errorf("role = %q, want admin", updated.Role) } // Invalid role should error. err = users.SetRole(user.ID, "superadmin") if err == nil { t.Error("invalid role should return error") } } func TestDeleteUser(t *testing.T) { db := testDB(t) users := NewUserStore(db) faves := NewFaveStore(db) Argon2Memory = 1024 Argon2Time = 1 defer func() { Argon2Memory = 65536; Argon2Time = 3 }() user, _ := users.Create("deleteme", "password123", "user") faves.Create(user.ID, "Fave 1", "", "", "", "public") faves.Create(user.ID, "Fave 2", "", "", "", "public") err := users.Delete(user.ID) if err != nil { t.Fatalf("delete user: %v", err) } // User should be gone. _, err = users.GetByUsername("deleteme") if err != ErrUserNotFound { t.Errorf("expected ErrUserNotFound, got %v", err) } // Faves should be cascade-deleted. list, total, _ := faves.ListByUser(user.ID, 10, 0) if total != 0 || len(list) != 0 { t.Errorf("faves should be cascade-deleted: total=%d, len=%d", total, len(list)) } // Deleting non-existent user should return error. err = users.Delete(99999) if err != ErrUserNotFound { t.Errorf("delete nonexistent: %v, want ErrUserNotFound", err) } }