favoritter/internal/store/user_test.go
Ole-Morten Duesund 254573316a feat: add admin role management and user deletion
Admins can now change user roles and permanently delete user accounts.

- New SetRole store method with validation (user/admin only)
- New Delete store method — cascades via foreign keys to sessions,
  faves, and fave_tags
- handleAdminSetRole: change role with self-modification prevention
- handleAdminDeleteUser: permanent deletion with image cleanup from
  disk before cascade delete, self-deletion prevention
- admin_users.html: role dropdown with save button per user row,
  delete button with hx-confirm for safety
- Routes: POST /admin/users/{id}/role, POST /admin/users/{id}/delete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:18:00 +02:00

273 lines
6.2 KiB
Go

// 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)
}
}