package accountapiimp import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/tech/sendico/pkg/db/repository/builder" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/server/interface/api/srequest" "go.mongodb.org/mongo-driver/v2/bson" "go.uber.org/zap" ) // Helper function to create string pointers func stringPtr(s string) *string { return &s } // TestTimezoneValidation tests timezone validation logic separately func TestTimezoneValidation(t *testing.T) { t.Run("ValidTimezones", func(t *testing.T) { validTimezones := []string{ "UTC", "America/New_York", "Europe/London", "Asia/Tokyo", "Australia/Sydney", } for _, tz := range validTimezones { t.Run(tz, func(t *testing.T) { _, err := time.LoadLocation(tz) assert.NoError(t, err, "Timezone %s should be valid", tz) }) } }) t.Run("InvalidTimezones", func(t *testing.T) { invalidTimezones := []string{ "Invalid/Timezone", "Not/A/Timezone", "BadTimezone", "America/NotACity", } for _, tz := range invalidTimezones { t.Run(tz, func(t *testing.T) { _, err := time.LoadLocation(tz) assert.Error(t, err, "Timezone %s should be invalid", tz) }) } }) } // TestCreateValidSignupRequest tests the helper function for creating valid requests func TestCreateValidSignupRequest(t *testing.T) { request := srequest.Signup{ Account: model.AccountData{ LoginData: model.LoginData{ UserDataBase: model.UserDataBase{ Login: "test@example.com", }, Password: "TestPassword123!", }, Describable: model.Describable{ Name: "Test User", }, }, Organization: model.Describable{ Name: "Test Organization", }, OrganizationTimeZone: "UTC", OwnerRole: model.Describable{ Name: "Owner", }, } // Validate the request structure assert.Equal(t, "test@example.com", request.Account.Login) assert.Equal(t, "TestPassword123!", request.Account.Password) assert.Equal(t, "Test User", request.Account.Name) assert.Equal(t, "Test Organization", request.Organization.Name) assert.Equal(t, "UTC", request.OrganizationTimeZone) } // TestSignupRequestValidation tests various signup request validation scenarios func TestSignupRequestValidation(t *testing.T) { t.Run("ValidRequest", func(t *testing.T) { request := srequest.Signup{ Account: model.AccountData{ LoginData: model.LoginData{ UserDataBase: model.UserDataBase{ Login: "test@example.com", }, Password: "TestPassword123!", }, Describable: model.Describable{ Name: "Test User", }, }, Organization: model.Describable{ Name: "Test Organization", }, OrganizationTimeZone: "UTC", } // Basic validation - all required fields present assert.NotEmpty(t, request.Account.Login) assert.NotEmpty(t, request.Account.Password) assert.NotEmpty(t, request.Account.Name) assert.NotEmpty(t, request.Organization.Name) assert.NotEmpty(t, request.OrganizationTimeZone) }) t.Run("EmailFormats", func(t *testing.T) { validEmails := []string{ "test@example.com", "user.name@example.com", "user+tag@example.org", "test123@domain.co.uk", } for _, email := range validEmails { t.Run(email, func(t *testing.T) { request := srequest.Signup{ Account: model.AccountData{ LoginData: model.LoginData{ UserDataBase: model.UserDataBase{ Login: email, }, }, }, } assert.Equal(t, email, request.Account.Login) assert.Contains(t, email, "@") assert.Contains(t, email, ".") }) } }) t.Run("PasswordComplexity", func(t *testing.T) { passwordTests := []struct { name string password string valid bool }{ {"Strong", "TestPassword123!", true}, {"WithNumbers", "MyPass123!", true}, {"WithSymbols", "Complex@Pass1", true}, {"TooShort", "Test1!", false}, {"NoNumbers", "TestPassword!", false}, {"NoSymbols", "TestPassword123", false}, {"NoUppercase", "testpassword123!", false}, {"NoLowercase", "TESTPASSWORD123!", false}, } for _, tt := range passwordTests { t.Run(tt.name, func(t *testing.T) { request := srequest.Signup{ Account: model.AccountData{ LoginData: model.LoginData{ Password: tt.password, }, }, } // Basic structure validation assert.Equal(t, tt.password, request.Account.Password) if tt.valid { assert.True(t, len(tt.password) >= 8, "Password should be at least 8 characters") } else { // For invalid passwords, at least one condition should fail hasDigit := false hasUpper := false hasLower := false hasSpecial := false for _, char := range tt.password { switch { case char >= '0' && char <= '9': hasDigit = true case char >= 'A' && char <= 'Z': hasUpper = true case char >= 'a' && char <= 'z': hasLower = true case char >= '!' && char <= '/' || char >= ':' && char <= '@': hasSpecial = true } } // At least one requirement should fail for invalid passwords if len(tt.password) >= 8 { assert.False(t, hasDigit && hasUpper && hasLower && hasSpecial, "Password %s should fail at least one requirement", tt.password) } } }) } }) } // TestAccountDataToAccount tests the ToAccount method func TestAccountDataToAccount(t *testing.T) { accountData := model.AccountData{ LoginData: model.LoginData{ UserDataBase: model.UserDataBase{ Login: "test@example.com", }, Password: "TestPassword123!", }, Describable: model.Describable{ Name: "Test User", }, } account := accountData.ToAccount() assert.Equal(t, accountData.Login, account.Login) assert.Equal(t, accountData.Password, account.Password) assert.Equal(t, accountData.Name, account.Name) // Verify the account has proper structure assert.NotNil(t, account) assert.IsType(t, &model.Account{}, account) } // TestColorValidation tests that colors are properly formatted func TestColorValidation(t *testing.T) { validColors := []string{ "#FF0000", // Red "#00FF00", // Green "#0000FF", // Blue "#FFFFFF", // White "#000000", // Black "#FF8000", // Orange } for _, color := range validColors { t.Run(color, func(t *testing.T) { colorPtr := stringPtr(color) assert.NotNil(t, colorPtr) assert.Equal(t, color, *colorPtr) assert.True(t, len(color) == 7, "Color should be 7 characters long") assert.True(t, color[0] == '#', "Color should start with #") }) } } type stubAccountDB struct { result *model.Account err error } func (s *stubAccountDB) GetByEmail(ctx context.Context, email string) (*model.Account, error) { return s.result, s.err } func (s *stubAccountDB) GetByToken(ctx context.Context, email string) (*model.Account, error) { return nil, merrors.NotImplemented("stub") } func (s *stubAccountDB) GetAccountsByRefs(ctx context.Context, orgRef bson.ObjectID, refs []bson.ObjectID) ([]model.Account, error) { return nil, merrors.NotImplemented("stub") } func (s *stubAccountDB) Create(ctx context.Context, object *model.Account) error { return merrors.NotImplemented("stub") } func (s *stubAccountDB) InsertMany(ctx context.Context, objects []*model.Account) error { return merrors.NotImplemented("stub") } func (s *stubAccountDB) Get(ctx context.Context, objectRef bson.ObjectID, result *model.Account) error { return merrors.NotImplemented("stub") } func (s *stubAccountDB) Update(ctx context.Context, object *model.Account) error { return merrors.NotImplemented("stub") } func (s *stubAccountDB) Patch(ctx context.Context, objectRef bson.ObjectID, patch builder.Patch) error { return merrors.NotImplemented("stub") } func (s *stubAccountDB) Delete(ctx context.Context, objectRef bson.ObjectID) error { return merrors.NotImplemented("stub") } func (s *stubAccountDB) DeleteMany(ctx context.Context, query builder.Query) error { return merrors.NotImplemented("stub") } func (s *stubAccountDB) DeleteCascade(ctx context.Context, objectRef bson.ObjectID) error { return merrors.NotImplemented("stub") } func (s *stubAccountDB) FindOne(ctx context.Context, query builder.Query, result *model.Account) error { return merrors.NotImplemented("stub") } func TestEnsureLoginAvailable(t *testing.T) { ctx := context.Background() logger := zap.NewNop() t.Run("available", func(t *testing.T) { api := &AccountAPI{ logger: logger, db: &stubAccountDB{ err: merrors.ErrNoData, }, } assert.NoError(t, api.ensureLoginAvailable(ctx, "new@example.com")) }) t.Run("taken", func(t *testing.T) { api := &AccountAPI{ logger: logger, db: &stubAccountDB{ result: &model.Account{}, }, } err := api.ensureLoginAvailable(ctx, "used@example.com") assert.Error(t, err) assert.True(t, errors.Is(err, merrors.ErrDataConflict)) }) t.Run("invalid login", func(t *testing.T) { api := &AccountAPI{ logger: logger, db: &stubAccountDB{ err: merrors.ErrNoData, }, } err := api.ensureLoginAvailable(ctx, " ") assert.Error(t, err) assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) }) t.Run("db error", func(t *testing.T) { api := &AccountAPI{ logger: logger, db: &stubAccountDB{ err: errors.New("boom"), }, } err := api.ensureLoginAvailable(ctx, "err@example.com") assert.EqualError(t, err, "boom") }) }