Files
sendico/api/server/internal/server/accountapiimp/signup_test.go
Stephan D c6a56071b5
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
+signup +login
2025-11-17 20:16:45 +01:00

356 lines
9.2 KiB
Go

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/bson/primitive"
"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 primitive.ObjectID, refs []primitive.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 primitive.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 primitive.ObjectID, patch builder.Patch) error {
return merrors.NotImplemented("stub")
}
func (s *stubAccountDB) Delete(ctx context.Context, objectRef primitive.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 primitive.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")
})
}