package user_test import ( "context" "encoding/json" "fmt" "testing" "time" "github.com/food-ai/backend/internal/domain/user" ) func ptrStr(s string) *string { return &s } func ptrInt(i int) *int { return &i } func ptrFloat(f float64) *float64 { return &f } // mockUserRepo is an in-test mock implementing user.UserRepository. type mockUserRepo struct { getByIDFn func(ctx context.Context, id string) (*user.User, error) updateFn func(ctx context.Context, id string, req user.UpdateProfileRequest) (*user.User, error) updateInTxFn func(ctx context.Context, id string, profileReq user.UpdateProfileRequest, caloriesReq *user.UpdateProfileRequest) (*user.User, error) upsertByFirebaseUIDFn func(ctx context.Context, uid, email, name, avatarURL string) (*user.User, error) setRefreshTokenFn func(ctx context.Context, id, token string, expiresAt time.Time) error findByRefreshTokenFn func(ctx context.Context, token string) (*user.User, error) clearRefreshTokenFn func(ctx context.Context, id string) error } func (m *mockUserRepo) UpsertByFirebaseUID(ctx context.Context, uid, email, name, avatarURL string) (*user.User, error) { return m.upsertByFirebaseUIDFn(ctx, uid, email, name, avatarURL) } func (m *mockUserRepo) GetByID(ctx context.Context, id string) (*user.User, error) { return m.getByIDFn(ctx, id) } func (m *mockUserRepo) Update(ctx context.Context, id string, req user.UpdateProfileRequest) (*user.User, error) { return m.updateFn(ctx, id, req) } func (m *mockUserRepo) UpdateInTx(ctx context.Context, id string, profileReq user.UpdateProfileRequest, caloriesReq *user.UpdateProfileRequest) (*user.User, error) { return m.updateInTxFn(ctx, id, profileReq, caloriesReq) } func (m *mockUserRepo) SetRefreshToken(ctx context.Context, id, token string, expiresAt time.Time) error { return m.setRefreshTokenFn(ctx, id, token, expiresAt) } func (m *mockUserRepo) FindByRefreshToken(ctx context.Context, token string) (*user.User, error) { return m.findByRefreshTokenFn(ctx, token) } func (m *mockUserRepo) ClearRefreshToken(ctx context.Context, id string) error { return m.clearRefreshTokenFn(ctx, id) } func TestGetProfile_Success(t *testing.T) { repo := &mockUserRepo{ getByIDFn: func(ctx context.Context, id string) (*user.User, error) { return &user.User{ID: id, Email: "test@example.com", Plan: "free"}, nil }, } svc := user.NewService(repo) u, getError := svc.GetProfile(context.Background(), "user-1") if getError != nil { t.Fatalf("unexpected error: %v", getError) } if u.ID != "user-1" { t.Errorf("expected user-1, got %s", u.ID) } } func TestGetProfile_NotFound(t *testing.T) { repo := &mockUserRepo{ getByIDFn: func(ctx context.Context, id string) (*user.User, error) { return nil, fmt.Errorf("not found") }, } svc := user.NewService(repo) _, getError := svc.GetProfile(context.Background(), "nonexistent") if getError == nil { t.Fatal("expected error") } } func TestUpdateProfile_NameOnly(t *testing.T) { repo := &mockUserRepo{ updateFn: func(ctx context.Context, id string, req user.UpdateProfileRequest) (*user.User, error) { return &user.User{ID: id, Name: *req.Name, Plan: "free"}, nil }, } svc := user.NewService(repo) u, updateError := svc.UpdateProfile(context.Background(), "user-1", user.UpdateProfileRequest{Name: ptrStr("New Name")}) if updateError != nil { t.Fatalf("unexpected error: %v", updateError) } if u.Name != "New Name" { t.Errorf("expected 'New Name', got %q", u.Name) } } func TestUpdateProfile_WithCaloriesRecalculation(t *testing.T) { dob := time.Now().AddDate(-30, 0, 0).Format("2006-01-02") profileReq := user.UpdateProfileRequest{ HeightCM: ptrInt(180), WeightKG: ptrFloat(80), DateOfBirth: &dob, Gender: ptrStr("male"), Activity: ptrStr("moderate"), Goal: ptrStr("maintain"), } finalUser := &user.User{ ID: "user-1", HeightCM: ptrInt(180), WeightKG: ptrFloat(80), DateOfBirth: &dob, Gender: ptrStr("male"), Activity: ptrStr("moderate"), Goal: ptrStr("maintain"), DailyCalories: ptrInt(2759), Plan: "free", } repo := &mockUserRepo{ // Service calls GetByID first to merge current values for calorie calculation getByIDFn: func(ctx context.Context, id string) (*user.User, error) { return &user.User{ID: id, Plan: "free"}, nil }, updateInTxFn: func(ctx context.Context, id string, pReq user.UpdateProfileRequest, cReq *user.UpdateProfileRequest) (*user.User, error) { if cReq == nil { t.Error("expected caloriesReq to be non-nil") } else if *cReq.DailyCalories != 2759 { t.Errorf("expected calories 2759, got %d", *cReq.DailyCalories) } return finalUser, nil }, } svc := user.NewService(repo) u, updateError := svc.UpdateProfile(context.Background(), "user-1", profileReq) if updateError != nil { t.Fatalf("unexpected error: %v", updateError) } if u.DailyCalories == nil { t.Fatal("expected daily_calories to be set") } if *u.DailyCalories != 2759 { t.Errorf("expected 2759, got %d", *u.DailyCalories) } } func TestUpdateProfile_InvalidHeight_TooLow(t *testing.T) { svc := user.NewService(&mockUserRepo{}) _, validationError := svc.UpdateProfile(context.Background(), "user-1", user.UpdateProfileRequest{HeightCM: ptrInt(50)}) if validationError == nil { t.Fatal("expected validation error") } } func TestUpdateProfile_InvalidHeight_TooHigh(t *testing.T) { svc := user.NewService(&mockUserRepo{}) _, validationError := svc.UpdateProfile(context.Background(), "user-1", user.UpdateProfileRequest{HeightCM: ptrInt(300)}) if validationError == nil { t.Fatal("expected validation error") } } func TestUpdateProfile_InvalidWeight_TooLow(t *testing.T) { svc := user.NewService(&mockUserRepo{}) _, validationError := svc.UpdateProfile(context.Background(), "user-1", user.UpdateProfileRequest{WeightKG: ptrFloat(10)}) if validationError == nil { t.Fatal("expected validation error") } } func TestUpdateProfile_InvalidWeight_TooHigh(t *testing.T) { svc := user.NewService(&mockUserRepo{}) _, validationError := svc.UpdateProfile(context.Background(), "user-1", user.UpdateProfileRequest{WeightKG: ptrFloat(400)}) if validationError == nil { t.Fatal("expected validation error") } } func TestUpdateProfile_InvalidDOB_TooRecent(t *testing.T) { svc := user.NewService(&mockUserRepo{}) dob := time.Now().AddDate(-5, 0, 0).Format("2006-01-02") _, validationError := svc.UpdateProfile(context.Background(), "user-1", user.UpdateProfileRequest{DateOfBirth: &dob}) if validationError == nil { t.Fatal("expected validation error") } } func TestUpdateProfile_InvalidDOB_TooOld(t *testing.T) { svc := user.NewService(&mockUserRepo{}) dob := time.Now().AddDate(-150, 0, 0).Format("2006-01-02") _, validationError := svc.UpdateProfile(context.Background(), "user-1", user.UpdateProfileRequest{DateOfBirth: &dob}) if validationError == nil { t.Fatal("expected validation error") } } func TestUpdateProfile_InvalidDOB_BadFormat(t *testing.T) { svc := user.NewService(&mockUserRepo{}) dob := "not-a-date" _, validationError := svc.UpdateProfile(context.Background(), "user-1", user.UpdateProfileRequest{DateOfBirth: &dob}) if validationError == nil { t.Fatal("expected validation error") } } func TestUpdateProfile_InvalidGender(t *testing.T) { svc := user.NewService(&mockUserRepo{}) _, validationError := svc.UpdateProfile(context.Background(), "user-1", user.UpdateProfileRequest{Gender: ptrStr("other")}) if validationError == nil { t.Fatal("expected validation error") } } func TestUpdateProfile_InvalidActivity(t *testing.T) { svc := user.NewService(&mockUserRepo{}) _, validationError := svc.UpdateProfile(context.Background(), "user-1", user.UpdateProfileRequest{Activity: ptrStr("extreme")}) if validationError == nil { t.Fatal("expected validation error") } } func TestUpdateProfile_InvalidGoal(t *testing.T) { svc := user.NewService(&mockUserRepo{}) _, validationError := svc.UpdateProfile(context.Background(), "user-1", user.UpdateProfileRequest{Goal: ptrStr("bulk")}) if validationError == nil { t.Fatal("expected validation error") } } func TestUpdateProfile_Preferences(t *testing.T) { prefs := json.RawMessage(`{"cuisines":["russian"]}`) repo := &mockUserRepo{ updateFn: func(ctx context.Context, id string, req user.UpdateProfileRequest) (*user.User, error) { return &user.User{ ID: id, Plan: "free", Preferences: *req.Preferences, UpdatedAt: time.Now(), }, nil }, } svc := user.NewService(repo) u, updateError := svc.UpdateProfile(context.Background(), "user-1", user.UpdateProfileRequest{Preferences: &prefs}) if updateError != nil { t.Fatalf("unexpected error: %v", updateError) } if string(u.Preferences) != `{"cuisines":["russian"]}` { t.Errorf("unexpected preferences: %s", u.Preferences) } }