587 lines
No EOL
16 KiB
Text
587 lines
No EOL
16 KiB
Text
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")
|
|
} |