package handler import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/dhao2001/mygo/internal/middleware" "github.com/dhao2001/mygo/internal/model" "github.com/dhao2001/mygo/internal/repository" "github.com/dhao2001/mygo/internal/service" ) func setupAdminRouter(t *testing.T) (*gin.Engine, repository.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{}, &model.Session{}, &model.Credential{}); err != nil { t.Fatalf("migrate: %v", err) } secret := []byte("test-secret") userRepo := repository.NewUserRepository(db) authService := service.NewAuthService( userRepo, repository.NewSessionRepository(db), repository.NewCredentialRepository(db), secret, 15*time.Minute, 7*24*time.Hour, ) authHandler := NewAuthHandler(authService) adminHandler := NewAdminHandler(userRepo) gin.SetMode(gin.TestMode) r := gin.New() auth := r.Group("/api/v1/auth") { auth.POST("/register", authHandler.Register) auth.POST("/login", authHandler.Login) } admin := r.Group("/api/v1/admin") admin.Use(middleware.AuthRequired(secret)) admin.Use(middleware.AdminRequired(userRepo)) { admin.GET("/users", adminHandler.ListUsers) admin.GET("/users/:id", adminHandler.GetUser) admin.DELETE("/users/:id", adminHandler.DeleteUser) } return r, userRepo } // registerHelper creates a user via the HTTP endpoint and returns the user ID. func registerHelper(t *testing.T, r *gin.Engine, username, email, password string) string { t.Helper() body, _ := json.Marshal(gin.H{ "username": username, "email": email, "password": password, }) req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusCreated { t.Fatalf("register %s: status = %d, body = %s", username, rec.Code, rec.Body.String()) } var user model.User if err := json.Unmarshal(rec.Body.Bytes(), &user); err != nil { t.Fatalf("unmarshal register response: %v", err) } return user.ID } // loginHelper logs in a user and returns the access token. func loginHelper(t *testing.T, r *gin.Engine, email, password string) string { t.Helper() body, _ := json.Marshal(gin.H{"email": email, "password": password}) req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("login %s: status = %d, body = %s", email, rec.Code, rec.Body.String()) } var pair service.TokenPair if err := json.Unmarshal(rec.Body.Bytes(), &pair); err != nil { t.Fatalf("unmarshal login response: %v", err) } return pair.AccessToken } // makeAdminHelper promotes a user to admin in the database. func makeAdminHelper(t *testing.T, userRepo repository.UserRepository, userID string) { t.Helper() user, err := userRepo.FindByID(context.Background(), userID) if err != nil { t.Fatalf("find user %s: %v", userID, err) } user.IsAdmin = true if err := userRepo.Update(context.Background(), user); err != nil { t.Fatalf("update user: %v", err) } } func TestAdminDeleteUser(t *testing.T) { r, userRepo := setupAdminRouter(t) adminID := registerHelper(t, r, "admin", "admin@example.com", "adminpass123") makeAdminHelper(t, userRepo, adminID) userID := registerHelper(t, r, "bob", "bob@example.com", "bobpass123") adminToken := loginHelper(t, r, "admin@example.com", "adminpass123") // Delete regular user as admin req := httptest.NewRequest(http.MethodDelete, "/api/v1/admin/users/"+userID, nil) req.Header.Set("Authorization", "Bearer "+adminToken) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusNoContent { t.Errorf("delete: status = %d, want %d; body = %s", rec.Code, http.StatusNoContent, rec.Body.String()) } // Verify regular user cannot login (soft-deleted) loginBody, _ := json.Marshal(gin.H{"email": "bob@example.com", "password": "bobpass123"}) req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(loginBody)) req.Header.Set("Content-Type", "application/json") rec = httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("login after soft-delete: status = %d, want %d; body = %s", rec.Code, http.StatusUnauthorized, rec.Body.String()) } } func TestAdminDeleteUserForbidden(t *testing.T) { r, userRepo := setupAdminRouter(t) adminID := registerHelper(t, r, "admin", "admin@example.com", "adminpass123") makeAdminHelper(t, userRepo, adminID) userID := registerHelper(t, r, "bob", "bob@example.com", "bobpass123") // Login as regular user (non-admin) userToken := loginHelper(t, r, "bob@example.com", "bobpass123") // Attempt to delete as non-admin req := httptest.NewRequest(http.MethodDelete, "/api/v1/admin/users/"+userID, nil) req.Header.Set("Authorization", "Bearer "+userToken) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Errorf("status = %d, want %d; body = %s", rec.Code, http.StatusForbidden, rec.Body.String()) } } func TestAdminDeleteUserUnauthorized(t *testing.T) { r, _ := setupAdminRouter(t) req := httptest.NewRequest(http.MethodDelete, "/api/v1/admin/users/some-id", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("status = %d, want %d; body = %s", rec.Code, http.StatusUnauthorized, rec.Body.String()) } } func TestAdminGetUser(t *testing.T) { r, userRepo := setupAdminRouter(t) adminID := registerHelper(t, r, "admin", "admin@example.com", "adminpass123") makeAdminHelper(t, userRepo, adminID) userID := registerHelper(t, r, "bob", "bob@example.com", "bobpass123") adminToken := loginHelper(t, r, "admin@example.com", "adminpass123") req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/users/"+userID, nil) req.Header.Set("Authorization", "Bearer "+adminToken) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("get user: status = %d, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String()) } var resp struct { ID string `json:"id"` Username string `json:"username"` Email string `json:"email"` IsAdmin bool `json:"is_admin"` Status string `json:"status"` } if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal: %v", err) } if resp.Username != "bob" { t.Errorf("username = %q, want %q", resp.Username, "bob") } if resp.Status != model.StatusActive { t.Errorf("status = %q, want %q", resp.Status, model.StatusActive) } } func TestAdminGetDeletedUser(t *testing.T) { r, userRepo := setupAdminRouter(t) adminID := registerHelper(t, r, "admin", "admin@example.com", "adminpass123") makeAdminHelper(t, userRepo, adminID) userID := registerHelper(t, r, "bob", "bob@example.com", "bobpass123") // Soft-delete regular user directly via repo if err := userRepo.Delete(context.Background(), userID); err != nil { t.Fatalf("soft-delete user: %v", err) } adminToken := loginHelper(t, r, "admin@example.com", "adminpass123") req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/users/"+userID, nil) req.Header.Set("Authorization", "Bearer "+adminToken) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("get deleted user: status = %d, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String()) } var resp struct { Status string `json:"status"` } if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal: %v", err) } if resp.Status != model.StatusAdminDeleted { t.Errorf("status = %q, want %q", resp.Status, model.StatusAdminDeleted) } } func TestAdminListIncludesDeleted(t *testing.T) { r, userRepo := setupAdminRouter(t) adminID := registerHelper(t, r, "admin", "admin@example.com", "adminpass123") makeAdminHelper(t, userRepo, adminID) // Create 2 regular users _ = registerHelper(t, r, "alice", "alice@example.com", "alicepass123") bobID := registerHelper(t, r, "bob", "bob@example.com", "bobpass123") // Soft-delete bob if err := userRepo.Delete(context.Background(), bobID); err != nil { t.Fatalf("soft-delete user: %v", err) } adminToken := loginHelper(t, r, "admin@example.com", "adminpass123") req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/users", nil) req.Header.Set("Authorization", "Bearer "+adminToken) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("list users: status = %d, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String()) } var resp struct { Users []struct { ID string `json:"id"` Status string `json:"status"` } `json:"users"` Total int64 `json:"total"` } if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal: %v", err) } // Total should be 3: admin + alice (active) + bob (soft-deleted) if resp.Total != 3 { t.Errorf("total = %d, want %d", resp.Total, 3) } // Verify both active and soft-deleted users appear foundAlice := false foundBob := false for _, u := range resp.Users { switch u.Status { case model.StatusActive: // alice or admin foundAlice = true case model.StatusAdminDeleted: if u.ID == bobID { foundBob = true } } } if !foundAlice { t.Error("active user not found in list") } if !foundBob { t.Error("soft-deleted user not found in list") } }