package repository import ( "context" "testing" "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/dhao2001/mygo/internal/model" ) func setupUserRepo(t *testing.T) UserRepository { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatalf("open db: %v", err) } if err := db.AutoMigrate(&model.User{}); err != nil { t.Fatalf("migrate: %v", err) } return NewUserRepository(db) } func TestUserRepository_Create(t *testing.T) { repo := setupUserRepo(t) ctx := context.Background() user := &model.User{ ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive, } if err := repo.Create(ctx, user); err != nil { t.Fatalf("Create = %v", err) } } func TestUserRepository_CreateDuplicateUsername(t *testing.T) { repo := setupUserRepo(t) ctx := context.Background() u1 := &model.User{ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive} u2 := &model.User{ID: "user-2", Username: "alice", Email: "alice2@example.com", PasswordHash: "hash", Status: model.StatusActive} if err := repo.Create(ctx, u1); err != nil { t.Fatalf("Create = %v", err) } err := repo.Create(ctx, u2) if err != model.ErrDuplicate { t.Fatalf("expected ErrDuplicate, got %v", err) } } func TestUserRepository_FindByID(t *testing.T) { repo := setupUserRepo(t) ctx := context.Background() user := &model.User{ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive} if err := repo.Create(ctx, user); err != nil { t.Fatalf("Create = %v", err) } found, err := repo.FindByID(ctx, "user-1") if err != nil { t.Fatalf("FindByID = %v", err) } if found.Username != "alice" { t.Errorf("username = %q, want %q", found.Username, "alice") } } func TestUserRepository_FindByIDNotFound(t *testing.T) { repo := setupUserRepo(t) ctx := context.Background() _, err := repo.FindByID(ctx, "nonexistent") if err != model.ErrNotFound { t.Fatalf("expected ErrNotFound, got %v", err) } } func TestUserRepository_FindByEmail(t *testing.T) { repo := setupUserRepo(t) ctx := context.Background() user := &model.User{ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive} if err := repo.Create(ctx, user); err != nil { t.Fatalf("Create = %v", err) } found, err := repo.FindByEmail(ctx, "alice@example.com") if err != nil { t.Fatalf("FindByEmail = %v", err) } if found.ID != "user-1" { t.Errorf("id = %q, want %q", found.ID, "user-1") } } func TestUserRepository_FindByUsername(t *testing.T) { repo := setupUserRepo(t) ctx := context.Background() user := &model.User{ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive} if err := repo.Create(ctx, user); err != nil { t.Fatalf("Create = %v", err) } found, err := repo.FindByUsername(ctx, "alice") if err != nil { t.Fatalf("FindByUsername = %v", err) } if found.Email != "alice@example.com" { t.Errorf("email = %q, want %q", found.Email, "alice@example.com") } } func TestUserRepository_Update(t *testing.T) { repo := setupUserRepo(t) ctx := context.Background() user := &model.User{ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive} if err := repo.Create(ctx, user); err != nil { t.Fatalf("Create = %v", err) } user.Username = "alice2" if err := repo.Update(ctx, user); err != nil { t.Fatalf("Update = %v", err) } found, err := repo.FindByID(ctx, "user-1") if err != nil { t.Fatalf("FindByID = %v", err) } if found.Username != "alice2" { t.Errorf("username = %q, want %q", found.Username, "alice2") } } func TestUserRepository_Delete(t *testing.T) { repo := setupUserRepo(t) ctx := context.Background() user := &model.User{ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive} if err := repo.Create(ctx, user); err != nil { t.Fatalf("Create = %v", err) } if err := repo.Delete(ctx, "user-1"); err != nil { t.Fatalf("Delete = %v", err) } _, err := repo.FindByID(ctx, "user-1") if err != model.ErrNotFound { t.Fatalf("expected ErrNotFound after delete, got %v", err) } } func TestUserRepository_List(t *testing.T) { repo := setupUserRepo(t) ctx := context.Background() for i := range 5 { user := &model.User{ ID: "user-" + string(rune('0'+i)), Username: "user" + string(rune('0'+i)), Email: "user" + string(rune('0'+i)) + "@example.com", PasswordHash: "hash", Status: model.StatusActive, } if err := repo.Create(ctx, user); err != nil { t.Fatalf("Create = %v", err) } } users, total, err := repo.List(ctx, 0, 3) if err != nil { t.Fatalf("List = %v", err) } if len(users) != 3 { t.Errorf("len(users) = %d, want %d", len(users), 3) } if total != 5 { t.Errorf("total = %d, want %d", total, 5) } } func TestUserRepository_SoftDelete(t *testing.T) { repo := setupUserRepo(t) ctx := context.Background() user := &model.User{ ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive, } if err := repo.Create(ctx, user); err != nil { t.Fatalf("Create = %v", err) } if err := repo.Delete(ctx, "user-1"); err != nil { t.Fatalf("Delete = %v", err) } // After soft-delete, FindByID should return ErrNotFound // because the status filter excludes soft-deleted users. _, err := repo.FindByID(ctx, "user-1") if err != model.ErrNotFound { t.Fatalf("expected ErrNotFound after soft-delete, got %v", err) } } func TestUserRepository_DisabledLogin(t *testing.T) { repo := setupUserRepo(t) ctx := context.Background() user := &model.User{ ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive, } if err := repo.Create(ctx, user); err != nil { t.Fatalf("Create = %v", err) } if err := repo.Delete(ctx, "user-1"); err != nil { t.Fatalf("Delete = %v", err) } // Soft-deleted users should not be findable by email, // preventing login for disabled accounts. _, err := repo.FindByEmail(ctx, "alice@example.com") if err != model.ErrNotFound { t.Fatalf("expected ErrNotFound for soft-deleted user login, got %v", err) } } func TestUserRepository_StatusFilterList(t *testing.T) { repo := setupUserRepo(t) ctx := context.Background() u1 := &model.User{ ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive, } if err := repo.Create(ctx, u1); err != nil { t.Fatalf("Create u1 = %v", err) } u2 := &model.User{ ID: "user-2", Username: "bob", Email: "bob@example.com", PasswordHash: "hash", Status: model.StatusActive, } if err := repo.Create(ctx, u2); err != nil { t.Fatalf("Create u2 = %v", err) } // Soft-delete bob if err := repo.Delete(ctx, "user-2"); err != nil { t.Fatalf("Delete u2 = %v", err) } // List with status filter should only return active users. users, total, err := repo.List(ctx, 0, 10) if err != nil { t.Fatalf("List = %v", err) } if len(users) != 1 { t.Fatalf("expected 1 active user, got %d", len(users)) } if total != 1 { t.Fatalf("expected total 1, got %d", total) } if users[0].ID != "user-1" { t.Errorf("expected active user user-1, got %s", users[0].ID) } } func TestUserRepository_DeleteIdempotent(t *testing.T) { repo := setupUserRepo(t) ctx := context.Background() user := &model.User{ ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive, } if err := repo.Create(ctx, user); err != nil { t.Fatalf("Create = %v", err) } // Soft-delete the same user twice should not error. if err := repo.Delete(ctx, "user-1"); err != nil { t.Fatalf("first Delete = %v", err) } if err := repo.Delete(ctx, "user-1"); err != nil { t.Fatalf("second Delete = %v", err) } }