package handler import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "time" "starter-kit/internal/adapter/persistence" "starter-kit/internal/domain/role" "starter-kit/internal/domain/user" "starter-kit/internal/service" "starter-kit/internal/transport/http/dto" "github.com/DATA-DOG/go-sqlmock" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "gorm.io/driver/mysql" "gorm.io/gorm" ) // mock user repository với khả năng hook type mockUserRepo struct { user.Repository // nhúng interface để implement tự động CreateFunc func(ctx context.Context, u *user.User) error GetByIDFunc func(ctx context.Context, id string) (*user.User, error) AddRoleFunc func(ctx context.Context, userID string, roleID int) error } func (m *mockUserRepo) Create(ctx context.Context, u *user.User) error { if m.CreateFunc != nil { return m.CreateFunc(ctx, u) } return nil } func (m *mockUserRepo) GetByID(ctx context.Context, id string) (*user.User, error) { if m.GetByIDFunc != nil { return m.GetByIDFunc(ctx, id) } return nil, nil } func (m *mockUserRepo) AddRole(ctx context.Context, userID string, roleID int) error { if m.AddRoleFunc != nil { return m.AddRoleFunc(ctx, userID, roleID) } return nil } func TestRegisterHandler(t *testing.T) { // Thiết lập gin.SetMode(gin.TestMode) // UUID cố định cho bài test testUserID := "123e4567-e89b-12d3-a456-426614174000" // Tạo mock database db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp)) if err != nil { t.Fatalf("Không thể tạo mock database: %v", err) } defer func() { _ = db.Close() }() // Kết nối GORM gormDB, err := gorm.Open(mysql.New(mysql.Config{ Conn: db, SkipInitializeWithVersion: true, }), &gorm.Config{}) if err != nil { t.Fatalf("Không thể kết nối GORM: %v", err) } // Tạo repositories thật sẽ kết nối với mock DB realUserRepo := persistence.NewUserRepository(gormDB) roleRepo := persistence.NewRoleRepository(gormDB) // Tạo mock repository với đầy đủ các phương thức cần thiết mockedUserRepo := &mockUserRepo{ Repository: realUserRepo, // delegate các phương thức còn lại CreateFunc: func(ctx context.Context, u *user.User) error { // Chú ý: Trong thực tế, ID sẽ được tạo bởi DB (uuid_generate_v4()) // Nhưng vì đây là test, chúng ta cần giả lập việc DB thiết lập ID sau khi INSERT // Gọi repository thật để thực thi SQL err := realUserRepo.Create(ctx, u) // Gán ID cố định sau khi tạo, giả lập việc DB tạo và trả về ID u.ID = testUserID return err }, GetByIDFunc: func(ctx context.Context, id string) (*user.User, error) { // Tạo user đủ thông tin với role đã preload userRole := &role.Role{ID: 1, Name: "user", Description: "Basic user role"} u := &user.User{ ID: testUserID, Username: "testuser", Email: "test@example.com", FullName: "Test User", AvatarURL: "", IsActive: true, Roles: []*role.Role{userRole}, // Gán role đã preload } return u, nil }, AddRoleFunc: func(ctx context.Context, userID string, roleID int) error { // Kiểm tra đảm bảo ID phù hợp if userID != testUserID { return fmt.Errorf("expected user ID %s but got %s", testUserID, userID) } // Khi chúng ta gọi AddRole của repo thật, nó sẽ thực thi câu lệnh SQL return realUserRepo.AddRole(ctx, userID, roleID) }, } // Tạo service với mock userRepo jwtSecret := "test-secret-key" authSvc := service.NewAuthService(mockedUserRepo, roleRepo, jwtSecret, time.Duration(15)*time.Minute) // Tạo handler authHandler := NewAuthHandler(authSvc) // Tạo router r := gin.Default() r.POST("/api/v1/auth/register", authHandler.Register) // Dữ liệu đăng ký registerData := dto.RegisterRequest{ Username: "testuser", Email: "test@example.com", Password: "password123", FullName: "Test User", } // Chuyển đổi dữ liệu thành JSON jsonData, err := json.Marshal(registerData) if err != nil { t.Fatalf("Lỗi chuyển đổi JSON: %v", err) } t.Run("Đăng ký tài khoản mới thành công", func(t *testing.T) { // Setup các mong đợi SQL match chính xác với GORM theo logs và UserRepository implementation // 1. Kiểm tra xem username đã tồn tại chưa (userRepo.GetByUsername) mock.ExpectQuery("SELECT \\* FROM `users` WHERE username = \\? ORDER BY `users`\\.`id` LIMIT \\?"). WithArgs("testuser", 1). WillReturnError(gorm.ErrRecordNotFound) // Username 'testuser' chưa tồn tại // 2. Kiểm tra xem email đã tồn tại chưa (userRepo.GetByEmail) mock.ExpectQuery("SELECT \\* FROM `users` WHERE email = \\? ORDER BY `users`\\.`id` LIMIT \\?"). WithArgs("test@example.com", 1). WillReturnError(gorm.ErrRecordNotFound) // Email 'test@example.com' chưa tồn tại // --- Sequence of operations after successful username/email checks and password hashing --- // 3. Transaction for userRepo.Create (Implicit transaction by GORM) mock.ExpectBegin() // 4. Tạo user mới (userRepo.Create) // Khi không đặt trước ID, GORM không đưa ID vào SQL, để DB tạo UUID tự động mock.ExpectExec("^INSERT INTO `users` \\(`username`,`email`,`password_hash`,`full_name`,`avatar_url`,`is_active`,`last_login_at`,`created_at`,`updated_at`,`deleted_at`\\) VALUES \\(\\?,\\?,\\?,\\?,\\?,\\?,\\?,\\?,\\?,\\?\\)"). WithArgs( "testuser", // username "test@example.com", // email sqlmock.AnyArg(), // password_hash "Test User", // full_name "", // avatar_url true, // is_active sqlmock.AnyArg(), // last_login_at sqlmock.AnyArg(), // created_at sqlmock.AnyArg(), // updated_at sqlmock.AnyArg(), // deleted_at ). WillReturnResult(sqlmock.NewResult(0, 1)) // UUID không có sequence ID, chỉ cần 1 row affected mock.ExpectCommit() // 5. Lấy role mặc định 'user' (roleRepo.GetByName) mock.ExpectQuery("SELECT \\* FROM `roles` WHERE name = \\? ORDER BY `roles`\\.`id` LIMIT \\?"). WithArgs("user", 1). WillReturnRows(sqlmock.NewRows([]string{"id", "name", "description", "created_at", "updated_at", "deleted_at"}). AddRow(1, "user", "Basic user role", time.Now(), time.Now(), nil)) // 6. Thêm role cho user (userRepo.AddRole -> user_roles table) // GORM's Create for user_roles có thể dùng 'INSERT ... ON CONFLICT' mock.ExpectExec("INSERT INTO `user_roles` \\(`user_id`, `role_id`\\) VALUES \\(\\?\\, \\?\\)"). WithArgs(testUserID, 1). // user_id (UUID string), role_id (int) WillReturnResult(sqlmock.NewResult(0, 1)) // Thêm thành công 1 row // Chú ý: Vì chúng ta đã override mockUserRepo.GetByID và mockUserRepo.AddRole // nên không cần mock SQL cho các query lấy thông tin user sau khi tạo // mockUserRepo.GetByID sẽ trả về user đã có role được preload // Tạo request req, _ := http.NewRequest("POST", "/api/v1/auth/register", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() // Thực thi request r.ServeHTTP(w, req) // Kiểm tra kết quả assert.Equal(t, http.StatusCreated, w.Code, "Status code phải là 201") // Parse JSON response var response dto.UserResponse err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err, "Parse JSON không có lỗi") // Kiểm tra thông tin phản hồi assert.Equal(t, registerData.Username, response.Username, "Username phải khớp") assert.Equal(t, registerData.Email, response.Email, "Email phải khớp") assert.Equal(t, registerData.FullName, response.FullName, "FullName phải khớp") assert.NotEmpty(t, response.ID, "ID không được rỗng") // Kiểm tra nếu có SQL expectations nào chưa được đáp ứng if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("Các expectations chưa được đáp ứng: %s", err) } }) }