add service
All checks were successful
Deploy Smart Search Backend / deploy (push) Successful in 1m47s
All checks were successful
Deploy Smart Search Backend / deploy (push) Successful in 1m47s
This commit is contained in:
156
pkg/validation/validation.go
Normal file
156
pkg/validation/validation.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"git.techease.ru/Smart-search/smart-search-back/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
MinPasswordLength = 8
|
||||
MaxPasswordLength = 128
|
||||
MaxEmailLength = 254
|
||||
MaxNameLength = 100
|
||||
MaxPhoneLength = 20
|
||||
MaxRequestTxtLen = 50000
|
||||
MaxFileSizeBytes = 10 * 1024 * 1024
|
||||
)
|
||||
|
||||
var (
|
||||
phoneRegex = regexp.MustCompile(`^\+?[1-9]\d{6,14}$`)
|
||||
)
|
||||
|
||||
func ValidateEmail(email string) error {
|
||||
if email == "" {
|
||||
return errors.NewBusinessError(errors.ValidationInvalidEmail, "email is required")
|
||||
}
|
||||
|
||||
if len(email) > MaxEmailLength {
|
||||
return errors.NewBusinessError(errors.ValidationInvalidEmail, "email is too long")
|
||||
}
|
||||
|
||||
_, err := mail.ParseAddress(email)
|
||||
if err != nil {
|
||||
return errors.NewBusinessError(errors.ValidationInvalidEmail, "invalid email format")
|
||||
}
|
||||
|
||||
parts := strings.Split(email, "@")
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
return errors.NewBusinessError(errors.ValidationInvalidEmail, "invalid email format")
|
||||
}
|
||||
|
||||
if strings.Contains(parts[1], "..") {
|
||||
return errors.NewBusinessError(errors.ValidationInvalidEmail, "invalid email format")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidatePassword(password string) error {
|
||||
if password == "" {
|
||||
return errors.NewBusinessError(errors.ValidationInvalidPassword, "password is required")
|
||||
}
|
||||
|
||||
if len(password) < MinPasswordLength {
|
||||
return errors.NewBusinessError(errors.ValidationInvalidPassword, "password must be at least 8 characters")
|
||||
}
|
||||
|
||||
if len(password) > MaxPasswordLength {
|
||||
return errors.NewBusinessError(errors.ValidationInvalidPassword, "password is too long")
|
||||
}
|
||||
|
||||
var hasUpper, hasLower, hasDigit bool
|
||||
for _, c := range password {
|
||||
switch {
|
||||
case unicode.IsUpper(c):
|
||||
hasUpper = true
|
||||
case unicode.IsLower(c):
|
||||
hasLower = true
|
||||
case unicode.IsDigit(c):
|
||||
hasDigit = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasUpper || !hasLower || !hasDigit {
|
||||
return errors.NewBusinessError(errors.ValidationInvalidPassword, "password must contain uppercase, lowercase and digit")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidatePhone(phone string) error {
|
||||
if phone == "" {
|
||||
return errors.NewBusinessError(errors.ValidationInvalidPhone, "phone is required")
|
||||
}
|
||||
|
||||
if len(phone) > MaxPhoneLength {
|
||||
return errors.NewBusinessError(errors.ValidationInvalidPhone, "phone is too long")
|
||||
}
|
||||
|
||||
cleaned := strings.ReplaceAll(phone, " ", "")
|
||||
cleaned = strings.ReplaceAll(cleaned, "-", "")
|
||||
cleaned = strings.ReplaceAll(cleaned, "(", "")
|
||||
cleaned = strings.ReplaceAll(cleaned, ")", "")
|
||||
|
||||
if !phoneRegex.MatchString(cleaned) {
|
||||
return errors.NewBusinessError(errors.ValidationInvalidPhone, "invalid phone format")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateName(name string) error {
|
||||
if name == "" {
|
||||
return errors.NewBusinessError(errors.ValidationInvalidName, "name is required")
|
||||
}
|
||||
|
||||
if len(name) > MaxNameLength {
|
||||
return errors.NewBusinessError(errors.ValidationInvalidName, "name is too long")
|
||||
}
|
||||
|
||||
trimmed := strings.TrimSpace(name)
|
||||
if trimmed == "" {
|
||||
return errors.NewBusinessError(errors.ValidationInvalidName, "name cannot be only whitespace")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateRequestTxt(txt string) error {
|
||||
if len(txt) > MaxRequestTxtLen {
|
||||
return errors.NewBusinessError(errors.ValidationRequestTooLong, "request text exceeds 50000 characters limit")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateFileSize(size int) error {
|
||||
if size > MaxFileSizeBytes {
|
||||
return errors.NewBusinessError(errors.ValidationFileTooLarge, "file size exceeds 10MB limit")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateRegistration(email, password, name, phone string) error {
|
||||
if err := ValidateEmail(email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ValidatePassword(password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ValidateName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ValidatePhone(phone); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
222
pkg/validation/validation_test.go
Normal file
222
pkg/validation/validation_test.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.techease.ru/Smart-search/smart-search-back/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidateEmail(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
email string
|
||||
wantErr bool
|
||||
wantCode string
|
||||
}{
|
||||
{"valid email", "test@example.com", false, ""},
|
||||
{"valid with subdomain", "test@sub.example.com", false, ""},
|
||||
{"valid with plus", "test+tag@example.com", false, ""},
|
||||
{"valid with dots", "test.name@example.com", false, ""},
|
||||
{"empty", "", true, errors.ValidationInvalidEmail},
|
||||
{"no at sign", "testexample.com", true, errors.ValidationInvalidEmail},
|
||||
{"no domain", "test@", true, errors.ValidationInvalidEmail},
|
||||
{"no local part", "@example.com", true, errors.ValidationInvalidEmail},
|
||||
{"double dots in domain", "test@example..com", true, errors.ValidationInvalidEmail},
|
||||
{"too long", strings.Repeat("a", 255) + "@example.com", true, errors.ValidationInvalidEmail},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateEmail(tt.email)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
assert.Equal(t, tt.wantCode, appErr.Code)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePassword(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
wantErr bool
|
||||
wantCode string
|
||||
}{
|
||||
{"valid password", "Abcd1234", false, ""},
|
||||
{"valid with special chars", "Abcd1234!", false, ""},
|
||||
{"empty", "", true, errors.ValidationInvalidPassword},
|
||||
{"too short", "Ab1", true, errors.ValidationInvalidPassword},
|
||||
{"no uppercase", "abcd1234", true, errors.ValidationInvalidPassword},
|
||||
{"no lowercase", "ABCD1234", true, errors.ValidationInvalidPassword},
|
||||
{"no digit", "Abcdefgh", true, errors.ValidationInvalidPassword},
|
||||
{"only digits", "12345678", true, errors.ValidationInvalidPassword},
|
||||
{"only lowercase", "abcdefgh", true, errors.ValidationInvalidPassword},
|
||||
{"only uppercase", "ABCDEFGH", true, errors.ValidationInvalidPassword},
|
||||
{"too long", strings.Repeat("Aa1", 50), true, errors.ValidationInvalidPassword},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidatePassword(tt.password)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
assert.Equal(t, tt.wantCode, appErr.Code)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePhone(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
phone string
|
||||
wantErr bool
|
||||
wantCode string
|
||||
}{
|
||||
{"valid international", "+1234567890", false, ""},
|
||||
{"valid with country code", "+79123456789", false, ""},
|
||||
{"valid without plus", "1234567890", false, ""},
|
||||
{"empty", "", true, errors.ValidationInvalidPhone},
|
||||
{"too short", "123", true, errors.ValidationInvalidPhone},
|
||||
{"letters", "abcdefgh", true, errors.ValidationInvalidPhone},
|
||||
{"too long", "+123456789012345678901", true, errors.ValidationInvalidPhone},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidatePhone(tt.phone)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
assert.Equal(t, tt.wantCode, appErr.Code)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
wantErr bool
|
||||
wantCode string
|
||||
}{
|
||||
{"valid name", "John Doe", false, ""},
|
||||
{"valid cyrillic", "Иван Иванов", false, ""},
|
||||
{"empty", "", true, errors.ValidationInvalidName},
|
||||
{"only whitespace", " ", true, errors.ValidationInvalidName},
|
||||
{"too long", strings.Repeat("a", 101), true, errors.ValidationInvalidName},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateName(tt.value)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
assert.Equal(t, tt.wantCode, appErr.Code)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRequestTxt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
txt string
|
||||
wantErr bool
|
||||
wantCode string
|
||||
}{
|
||||
{"valid short", "Test request", false, ""},
|
||||
{"empty is valid", "", false, ""},
|
||||
{"max length", strings.Repeat("a", MaxRequestTxtLen), false, ""},
|
||||
{"too long", strings.Repeat("a", MaxRequestTxtLen+1), true, errors.ValidationRequestTooLong},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateRequestTxt(tt.txt)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
assert.Equal(t, tt.wantCode, appErr.Code)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFileSize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
size int
|
||||
wantErr bool
|
||||
wantCode string
|
||||
}{
|
||||
{"zero", 0, false, ""},
|
||||
{"small file", 1024, false, ""},
|
||||
{"max size", MaxFileSizeBytes, false, ""},
|
||||
{"too large", MaxFileSizeBytes + 1, true, errors.ValidationFileTooLarge},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateFileSize(tt.size)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
assert.Equal(t, tt.wantCode, appErr.Code)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRegistration(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
email string
|
||||
password string
|
||||
userName string
|
||||
phone string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid", "test@example.com", "Abcd1234", "John Doe", "+1234567890", false},
|
||||
{"invalid email", "invalid", "Abcd1234", "John Doe", "+1234567890", true},
|
||||
{"invalid password", "test@example.com", "weak", "John Doe", "+1234567890", true},
|
||||
{"invalid name", "test@example.com", "Abcd1234", "", "+1234567890", true},
|
||||
{"invalid phone", "test@example.com", "Abcd1234", "John Doe", "abc", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateRegistration(tt.email, tt.password, tt.userName, tt.phone)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user