package handlers import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "veza-backend-api/internal/models" "veza-backend-api/internal/repository" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" ) func TestProfileHandler_GetProfile_Success(t *testing.T) { gin.SetMode(gin.TestMode) // Setup: Create real UserService with in-memory repository userRepo := repository.NewUserRepository() userService := services.NewUserService(userRepo) handler := NewProfileHandler(userService) // Create a test user userID := uuid.New() createdAt := time.Now() user := &models.User{ ID: userID, Username: "testuser", Email: "test@example.com", Avatar: "https://example.com/avatar.jpg", Bio: "Test bio", FirstName: "Test", LastName: "User", CreatedAt: createdAt, IsActive: true, IsVerified: true, IsPublic: true, } // Add user to repository err := userRepo.Create(user) assert.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/api/v1/users/"+userID.String()+"/profile", nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{{Key: "id", Value: userID.String()}} handler.GetProfile(c) assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.Contains(t, response, "profile") profile := response["profile"].(map[string]interface{}) assert.Equal(t, "testuser", profile["username"]) assert.Equal(t, "https://example.com/avatar.jpg", profile["avatar_url"]) assert.Equal(t, "Test bio", profile["bio"]) } func TestProfileHandler_GetProfile_InvalidID(t *testing.T) { gin.SetMode(gin.TestMode) userRepo := repository.NewUserRepository() userService := services.NewUserService(userRepo) handler := NewProfileHandler(userService) req := httptest.NewRequest(http.MethodGet, "/api/v1/users/invalid/profile", nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{{Key: "id", Value: "invalid"}} handler.GetProfile(c) assert.Equal(t, http.StatusBadRequest, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.Contains(t, response, "error") assert.Equal(t, "invalid user id", response["error"]) } func TestProfileHandler_GetProfile_UserNotFound(t *testing.T) { gin.SetMode(gin.TestMode) userRepo := repository.NewUserRepository() userService := services.NewUserService(userRepo) handler := NewProfileHandler(userService) randomID := uuid.New().String() req := httptest.NewRequest(http.MethodGet, "/api/v1/users/"+randomID+"/profile", nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{{Key: "id", Value: randomID}} handler.GetProfile(c) assert.Equal(t, http.StatusNotFound, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.Contains(t, response, "error") assert.Equal(t, "user not found", response["error"]) } func TestProfileHandler_GetProfile_OwnProfile(t *testing.T) { gin.SetMode(gin.TestMode) userRepo := repository.NewUserRepository() userService := services.NewUserService(userRepo) handler := NewProfileHandler(userService) userID := uuid.New() createdAt := time.Now() user := &models.User{ ID: userID, Username: "testuser", Email: "test@example.com", Avatar: "https://example.com/avatar.jpg", Bio: "Test bio", FirstName: "Test", LastName: "User", CreatedAt: createdAt, IsActive: true, IsVerified: true, IsPublic: true, } err := userRepo.Create(user) assert.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/api/v1/users/"+userID.String()+"/profile", nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{{Key: "id", Value: userID.String()}} c.Set("user_id", userID) handler.GetProfile(c) assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.Contains(t, response, "profile") profile := response["profile"].(map[string]interface{}) assert.Equal(t, "testuser", profile["username"]) // When viewing own profile, should include email // assert.Equal(t, "test@example.com", profile["email"]) // Profile struct does not have email assert.Equal(t, "Test", profile["first_name"]) assert.Equal(t, "User", profile["last_name"]) } func TestProfileHandler_UpdateProfile_Success(t *testing.T) { gin.SetMode(gin.TestMode) userRepo := repository.NewUserRepository() userService := services.NewUserService(userRepo) handler := NewProfileHandler(userService) userID := uuid.New() createdAt := time.Now() user := &models.User{ ID: userID, Username: "testuser", Email: "test@example.com", FirstName: "Test", LastName: "User", Bio: "Old bio", CreatedAt: createdAt, IsActive: true, IsVerified: true, IsPublic: true, } err := userRepo.Create(user) assert.NoError(t, err) reqBody := map[string]interface{}{ "first_name": "Updated", "last_name": "Name", "bio": "New bio", "location": "Paris", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPut, "/api/v1/users/"+userID.String()+"/profile", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{{Key: "id", Value: userID.String()}} c.Set("user_id", userID) handler.UpdateProfile(c) assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.Contains(t, response, "profile") } func TestProfileHandler_UpdateProfile_Unauthorized(t *testing.T) { gin.SetMode(gin.TestMode) userRepo := repository.NewUserRepository() userService := services.NewUserService(userRepo) handler := NewProfileHandler(userService) userID := uuid.New() // We need a valid ID for the path even if not auth reqBody := map[string]interface{}{ "first_name": "Updated", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPut, "/api/v1/users/"+userID.String()+"/profile", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{{Key: "id", Value: userID.String()}} // No user_id set - unauthorized handler.UpdateProfile(c) assert.Equal(t, http.StatusUnauthorized, w.Code) } func TestProfileHandler_UpdateProfile_Forbidden(t *testing.T) { gin.SetMode(gin.TestMode) userRepo := repository.NewUserRepository() userService := services.NewUserService(userRepo) handler := NewProfileHandler(userService) userID := uuid.New() reqBody := map[string]interface{}{ "first_name": "Updated", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPut, "/api/v1/users/"+userID.String()+"/profile", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{{Key: "id", Value: userID.String()}} c.Set("user_id", uuid.New()) // Different user ID handler.UpdateProfile(c) assert.Equal(t, http.StatusForbidden, w.Code) } func TestProfileHandler_UpdateProfile_InvalidUsername(t *testing.T) { gin.SetMode(gin.TestMode) userRepo := repository.NewUserRepository() userService := services.NewUserService(userRepo) handler := NewProfileHandler(userService) userID := uuid.New() user := &models.User{ ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true, } err := userRepo.Create(user) assert.NoError(t, err) reqBody := map[string]interface{}{ "username": "ab", // Too short } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPut, "/api/v1/users/"+userID.String()+"/profile", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{{Key: "id", Value: userID.String()}} c.Set("user_id", userID) handler.UpdateProfile(c) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestProfileHandler_UpdateProfile_InvalidBirthdate(t *testing.T) { gin.SetMode(gin.TestMode) userRepo := repository.NewUserRepository() userService := services.NewUserService(userRepo) handler := NewProfileHandler(userService) userID := uuid.New() user := &models.User{ ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true, } err := userRepo.Create(user) assert.NoError(t, err) // Birthdate that makes user less than 13 years old reqBody := map[string]interface{}{ "birthdate": time.Now().AddDate(-10, 0, 0).Format("2006-01-02"), } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPut, "/api/v1/users/"+userID.String()+"/profile", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{{Key: "id", Value: userID.String()}} c.Set("user_id", userID) handler.UpdateProfile(c) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestProfileHandler_UpdateProfile_UsernameTaken(t *testing.T) { gin.SetMode(gin.TestMode) userRepo := repository.NewUserRepository() userService := services.NewUserService(userRepo) handler := NewProfileHandler(userService) // Create first user user1ID := uuid.New() user1 := &models.User{ ID: user1ID, Username: "testuser", Email: "test@example.com", IsActive: true, } err := userRepo.Create(user1) assert.NoError(t, err) // Create second user user2ID := uuid.New() user2 := &models.User{ ID: user2ID, Username: "existinguser", Email: "existing@example.com", IsActive: true, } err = userRepo.Create(user2) assert.NoError(t, err) // Try to update user1 with user2's username reqBody := map[string]interface{}{ "username": "existinguser", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPut, "/api/v1/users/"+user1ID.String()+"/profile", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{{Key: "id", Value: user1ID.String()}} c.Set("user_id", user1ID) handler.UpdateProfile(c) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestProfileHandler_UpdateProfile_UsernameChangeLimit(t *testing.T) { gin.SetMode(gin.TestMode) userRepo := repository.NewUserRepository() userService := services.NewUserService(userRepo) handler := NewProfileHandler(userService) userID := uuid.New() recentChange := time.Now().AddDate(0, 0, -15) // 15 days ago user := &models.User{ ID: userID, Username: "testuser", Email: "test@example.com", UsernameChangedAt: &recentChange, IsActive: true, } err := userRepo.Create(user) assert.NoError(t, err) reqBody := map[string]interface{}{ "username": "newusername", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPut, "/api/v1/users/"+userID.String()+"/profile", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{{Key: "id", Value: userID.String()}} c.Set("user_id", userID) handler.UpdateProfile(c) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestProfileHandler_GetProfileByUsername_Success(t *testing.T) { gin.SetMode(gin.TestMode) userRepo := repository.NewUserRepository() userService := services.NewUserService(userRepo) handler := NewProfileHandler(userService) userID := uuid.New() createdAt := time.Now() user := &models.User{ ID: userID, Username: "testuser", Email: "test@example.com", Avatar: "https://example.com/avatar.jpg", Bio: "Test bio", FirstName: "Test", LastName: "User", Location: "Paris", CreatedAt: createdAt, IsActive: true, IsVerified: true, IsPublic: true, } err := userRepo.Create(user) assert.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/api/v1/users/by-username/testuser", nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{{Key: "username", Value: "testuser"}} handler.GetProfileByUsername(c) assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.Contains(t, response, "profile") profile := response["profile"].(map[string]interface{}) assert.Equal(t, userID.String(), profile["id"]) assert.Equal(t, "testuser", profile["username"]) assert.Equal(t, "Test", profile["first_name"]) assert.Equal(t, "User", profile["last_name"]) assert.Equal(t, "https://example.com/avatar.jpg", profile["avatar_url"]) assert.Equal(t, "Test bio", profile["bio"]) assert.Equal(t, "Paris", profile["location"]) } func TestProfileHandler_GetProfileByUsername_EmptyUsername(t *testing.T) { gin.SetMode(gin.TestMode) userRepo := repository.NewUserRepository() userService := services.NewUserService(userRepo) handler := NewProfileHandler(userService) req := httptest.NewRequest(http.MethodGet, "/api/v1/users/by-username/", nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{{Key: "username", Value: ""}} handler.GetProfileByUsername(c) assert.Equal(t, http.StatusBadRequest, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.Contains(t, response, "error") assert.Equal(t, "username required", response["error"]) } func TestProfileHandler_GetProfileByUsername_UserNotFound(t *testing.T) { gin.SetMode(gin.TestMode) userRepo := repository.NewUserRepository() userService := services.NewUserService(userRepo) handler := NewProfileHandler(userService) req := httptest.NewRequest(http.MethodGet, "/api/v1/users/by-username/nonexistent", nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{{Key: "username", Value: "nonexistent"}} handler.GetProfileByUsername(c) assert.Equal(t, http.StatusNotFound, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.Contains(t, response, "error") assert.Equal(t, "user not found", response["error"]) } func TestProfileHandler_GetProfileByUsername_PublicFieldsOnly(t *testing.T) { gin.SetMode(gin.TestMode) userRepo := repository.NewUserRepository() userService := services.NewUserService(userRepo) handler := NewProfileHandler(userService) userID := uuid.New() createdAt := time.Now() user := &models.User{ ID: userID, Username: "testuser", Email: "private@example.com", PasswordHash: "hashed_password", Avatar: "https://example.com/avatar.jpg", Bio: "Test bio", FirstName: "Test", LastName: "User", Location: "Paris", CreatedAt: createdAt, IsActive: true, IsVerified: true, } err := userRepo.Create(user) assert.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/api/v1/users/by-username/testuser", nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{{Key: "username", Value: "testuser"}} handler.GetProfileByUsername(c) assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.Contains(t, response, "profile") profile := response["profile"].(map[string]interface{}) // Email should NOT be in public profile assert.NotContains(t, profile, "email") // PasswordHash should NOT be in public profile assert.NotContains(t, profile, "password_hash") // Only public fields should be present assert.Contains(t, profile, "id") assert.Contains(t, profile, "username") assert.Contains(t, profile, "first_name") assert.Contains(t, profile, "last_name") assert.Contains(t, profile, "avatar_url") assert.Contains(t, profile, "bio") assert.Contains(t, profile, "location") assert.Contains(t, profile, "created_at") }