Files
Mazyar ce8a18aa8b
continuous-integration/drone/push Build is passing
Add unit tests for UpdateUserForm JSON unmarshalling
- Introduced `form_test.go` to validate the behavior of the `UpdateUserForm` struct's JSON unmarshalling, specifically for the `profile_image` field.
- Added tests to ensure omitted `profile_image` is not treated as an update, null values clear the image, and valid URLs are preserved.
- Enhanced the `UpdateUserForm` and `UpdateProfileForm` structs to include logic for distinguishing between omitted and explicitly null profile images during JSON unmarshalling.

This update improves the robustness of user profile updates by ensuring correct handling of profile image data in JSON requests, enhancing overall data integrity in the user management system.
2026-06-22 22:09:30 +03:30

208 lines
8.8 KiB
Go

package user
import (
"encoding/json"
"tm/pkg/response"
)
// User Registration DTOs
// CreateUserForm represents the data required to create a new user account
type CreateUserForm struct {
FullName string `json:"full_name" valid:"required,length(2|100)" example:"Admin User"` // Full name of the user
Username string `json:"username" valid:"required,alphanum,length(3|30)" example:"admin"` // Unique username (alphanumeric only)
Email string `json:"email" valid:"required,email" example:"admin@opplens.com"` // Valid email address
Password string `json:"password" valid:"required,length(8|128)" example:"Admin!1234"` // Password (minimum 8 characters)
Role string `json:"role" valid:"required,in(admin|manager|operator|viewer)" example:"admin"` // User role (admin, manager, operator, viewer)
Department *string `json:"department" valid:"optional,length(2|100)" example:"Information Technology"` // Optional department name
Position *string `json:"position" valid:"optional,length(2|100)" example:"Lead"` // Optional job position
Phone *string `json:"phone" valid:"optional,length(10|20)" example:"+18289784438"` // Optional phone number
ProfileImage *string `json:"profile_image" valid:"optional,url" example:""` // Optional profile image URL
}
// UpdateUserForm represents the data for updating an existing user account
type UpdateUserForm struct {
FullName *string `json:"full_name" valid:"optional,length(2|100)" example:"Admin User"` // Updated full name
Username *string `json:"username" valid:"optional,alphanum,length(3|30)" example:"admin"` // Updated username
Email *string `json:"email" valid:"optional,email" example:"admin@opplens.com"` // Updated email address
Role *string `json:"role" valid:"optional,in(admin|manager|operator|viewer)" example:"admin"` // Updated user role
Department *string `json:"department" valid:"optional,length(2|100)" example:"Information Technology"` // Updated department
Position *string `json:"position" valid:"optional,length(2|100)" example:"Lead"` // Updated position
Phone *string `json:"phone" valid:"optional,length(10|20)" example:"+18289784438"` // Updated phone number
ProfileImage *string `json:"profile_image" valid:"optional,url" example:""` // Updated profile image URL
profileImageSet bool
}
func unmarshalProfileImageField(data []byte, profileImage **string, profileImageSet *bool) ([]byte, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
profileImageRaw, ok := raw["profile_image"]
if ok {
*profileImageSet = true
if string(profileImageRaw) == "null" {
*profileImage = nil
} else {
var image string
if err := json.Unmarshal(profileImageRaw, &image); err != nil {
return nil, err
}
*profileImage = &image
}
delete(raw, "profile_image")
}
return json.Marshal(raw)
}
// UnmarshalJSON distinguishes omitted profile_image from explicit null to allow clearing the image.
func (f *UpdateUserForm) UnmarshalJSON(data []byte) error {
rest, err := unmarshalProfileImageField(data, &f.ProfileImage, &f.profileImageSet)
if err != nil {
return err
}
type alias UpdateUserForm
return json.Unmarshal(rest, (*alias)(f))
}
func (f *UpdateUserForm) isProfileImageSet() bool {
return f.profileImageSet
}
// User Authentication DTOs
// LoginForm represents the credentials required for user authentication
type LoginForm struct {
Username string `json:"username" valid:"required" example:"admin"` // Username or email address
Password string `json:"password" valid:"required" example:"Admin!1234"` // User password
DeviceToken string `json:"device_token" valid:"optional" example:"device_token"` // Device token
}
// RefreshTokenForm represents the refresh token required to generate new access tokens
type RefreshTokenForm struct {
RefreshToken string `json:"refresh_token" valid:"required" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` // Valid refresh token
}
// ChangePasswordForm represents the data required to change user password
type ChangePasswordForm struct {
OldPassword string `json:"old_password" valid:"required" example:"Admin!1234"` // Current password for verification
NewPassword string `json:"new_password" valid:"required,length(8|128)" example:"NewAdmin!1234"` // New password (minimum 8 characters)
}
// UpdateProfileForm represents the data for updating user profile information
type UpdateProfileForm struct {
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)" example:"Admin User"` // Updated full name
Department *string `json:"department,omitempty" valid:"optional,length(2|100)" example:"Information Technology"` // Updated department
Position *string `json:"position,omitempty" valid:"optional,length(2|100)" example:"Lead"` // Updated position
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+18289784438"` // Updated phone number
ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url" example:""` // Updated profile image URL
profileImageSet bool
}
// UnmarshalJSON distinguishes omitted profile_image from explicit null to allow clearing the image.
func (f *UpdateProfileForm) UnmarshalJSON(data []byte) error {
rest, err := unmarshalProfileImageField(data, &f.ProfileImage, &f.profileImageSet)
if err != nil {
return err
}
type alias UpdateProfileForm
return json.Unmarshal(rest, (*alias)(f))
}
func (f *UpdateProfileForm) isProfileImageSet() bool {
return f.profileImageSet
}
// ResetPasswordForm represents the email address for password reset
type ResetPasswordForm struct {
Email string `json:"email" valid:"required,email" example:"admin@opplens.com"` // Email address for password reset
}
// Admin DTOs
type ListUsersForm struct {
Search *string `query:"q" valid:"optional"`
Status *string `query:"status" valid:"optional,in(active|inactive|suspended)"`
Role *string `query:"role" valid:"optional,in(admin|manager|operator|viewer)"`
CompanyID *string `query:"company_id" valid:"optional"`
Limit *int `query:"limit" valid:"optional,range(1|100)"`
Offset *int `query:"offset" valid:"optional,range(0|1000000)"`
SortBy *string `query:"sort_by" valid:"optional,in(full_name|email|username|role|created_at|last_login_at)"`
SortOrder *string `query:"sort_order" valid:"optional,in(asc|desc)"`
}
type UpdateUserStatusForm struct {
Status string `json:"status" valid:"required,in(active|inactive|suspended)"`
}
// AdminResetPasswordResponse is returned when an admin resets another user's password.
type AdminResetPasswordResponse struct {
Password string `json:"password"`
}
type UpdateUserRoleForm struct {
Role string `json:"role" valid:"required,in(admin|manager|operator|viewer)"`
}
// Response DTOs
type UserResponse struct {
ID string `json:"id"`
FullName string `json:"full_name"`
Username string `json:"username"`
Email string `json:"email"`
Role string `json:"role"`
Status string `json:"status"`
Department *string `json:"department"`
Position *string `json:"position"`
Phone *string `json:"phone"`
ProfileImage *string `json:"profile_image"`
IsVerified bool `json:"is_verified"`
LastLoginAt *int64 `json:"last_login_at"`
UpdatedAt int64 `json:"updated_at"`
CreatedAt int64 `json:"created_at"`
DeviceToken []string `json:"-"`
}
type AuthResponse struct {
User *UserResponse `json:"user"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt int64 `json:"expires_at"`
}
type UserListResponse struct {
Users []*UserResponse `json:"users"`
Meta *response.Meta `json:"-"`
}
type SearchUsersForm struct {
Search *string `query:"search" valid:"optional"`
Status *string `query:"status" valid:"optional,in(active|inactive|suspended)"`
Role *string `query:"role" valid:"optional,in(admin|manager|operator|viewer)"`
}
// Helper function to convert User to UserResponse
func (u *User) ToResponse() *UserResponse {
return &UserResponse{
ID: u.ID.Hex(),
FullName: u.FullName,
Username: u.Username,
Email: u.Email,
Role: string(u.Role),
Status: string(u.Status),
Department: u.Department,
Position: u.Position,
Phone: u.Phone,
DeviceToken: u.DeviceToken,
ProfileImage: u.ProfileImage,
IsVerified: u.IsVerified,
LastLoginAt: u.LastLoginAt,
UpdatedAt: u.UpdatedAt,
CreatedAt: u.CreatedAt,
}
}