- Priority 1: Test NewSendEmailTask + NewSendPushTask (5 tests) - Priority 2: Test customHTTPErrorHandler — all 15+ branches (21 tests) - Priority 3: Extract Enqueuer interface + payload builders in worker pkg (5 tests) - Priority 4: Extract ClassifyFile/ComputeRelPath in migrate-encrypt (6 tests) - Priority 5: Define Handler interfaces, refactor to accept them, mock-based tests (14 tests) - Fix .gitignore: /worker instead of worker to stop ignoring internal/worker/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
234 lines
6.2 KiB
Go
234 lines
6.2 KiB
Go
package validator
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
govalidator "github.com/go-playground/validator/v10"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestValidatePasswordComplexity(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
password string
|
|
valid bool
|
|
}{
|
|
{"valid password", "Password1", true},
|
|
{"valid complex password", "MyP@ssw0rd!", true},
|
|
{"missing uppercase", "password1", false},
|
|
{"missing lowercase", "PASSWORD1", false},
|
|
{"missing digit", "Password", false},
|
|
{"only digits", "12345678", false},
|
|
{"only lowercase", "abcdefgh", false},
|
|
{"only uppercase", "ABCDEFGH", false},
|
|
{"empty string", "", false},
|
|
{"single valid char each", "aA1", true},
|
|
{"unicode uppercase with digit and lower", "Über1abc", true},
|
|
}
|
|
|
|
v := govalidator.New()
|
|
v.RegisterValidation("password_complexity", validatePasswordComplexity)
|
|
|
|
type testStruct struct {
|
|
Password string `validate:"password_complexity"`
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
s := testStruct{Password: tc.password}
|
|
err := v.Struct(s)
|
|
if tc.valid && err != nil {
|
|
t.Errorf("expected password %q to be valid, got error: %v", tc.password, err)
|
|
}
|
|
if !tc.valid && err == nil {
|
|
t.Errorf("expected password %q to be invalid, got nil error", tc.password)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidatePasswordComplexityWithMinLength(t *testing.T) {
|
|
v := govalidator.New()
|
|
v.RegisterValidation("password_complexity", validatePasswordComplexity)
|
|
|
|
type request struct {
|
|
Password string `validate:"required,min=8,password_complexity"`
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
password string
|
|
valid bool
|
|
}{
|
|
{"valid 8+ chars with complexity", "Abcdefg1", true},
|
|
{"too short but complex", "Ab1", false},
|
|
{"long but no uppercase", "abcdefgh1", false},
|
|
{"long but no lowercase", "ABCDEFGH1", false},
|
|
{"long but no digit", "Abcdefghi", false},
|
|
{"empty", "", false},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := request{Password: tc.password}
|
|
err := v.Struct(r)
|
|
if tc.valid && err != nil {
|
|
t.Errorf("expected %q to be valid, got error: %v", tc.password, err)
|
|
}
|
|
if !tc.valid && err == nil {
|
|
t.Errorf("expected %q to be invalid, got nil", tc.password)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatMessagePasswordComplexity(t *testing.T) {
|
|
cv := NewCustomValidator()
|
|
|
|
type request struct {
|
|
Password string `json:"password" validate:"required,min=8,password_complexity"`
|
|
}
|
|
|
|
r := request{Password: "lowercase1"}
|
|
err := cv.Validate(r)
|
|
if err == nil {
|
|
t.Fatal("expected validation error for password without uppercase")
|
|
}
|
|
|
|
resp := FormatValidationErrors(err)
|
|
if resp == nil {
|
|
t.Fatal("expected non-nil error response")
|
|
}
|
|
|
|
field, ok := resp.Fields["password"]
|
|
if !ok {
|
|
t.Fatal("expected 'password' field in error response")
|
|
}
|
|
|
|
expectedMsg := "Password must be at least 8 characters with at least one uppercase letter, one lowercase letter, and one digit"
|
|
if field.Message != expectedMsg {
|
|
t.Errorf("expected message %q, got %q", expectedMsg, field.Message)
|
|
}
|
|
|
|
if field.Tag != "password_complexity" {
|
|
t.Errorf("expected tag 'password_complexity', got %q", field.Tag)
|
|
}
|
|
}
|
|
|
|
func TestPasswordComplexity_AdditionalCases(t *testing.T) {
|
|
cv := NewCustomValidator()
|
|
|
|
type request struct {
|
|
Password string `json:"password" validate:"required,min=8,password_complexity"`
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
pw string
|
|
valid bool
|
|
}{
|
|
{"no uppercase no digit", "password", false},
|
|
{"no lowercase", "PASSWORD1", false},
|
|
{"no digit", "Password", false},
|
|
{"too short", "Pass1", false},
|
|
{"valid standard", "Password1", true},
|
|
{"valid with special chars", "P@ssw0rd", true},
|
|
{"spaces with complexity", "Pass 1234", true},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := request{Password: tc.pw}
|
|
err := cv.Validate(r)
|
|
if tc.valid {
|
|
assert.NoError(t, err, "expected %q to be valid", tc.pw)
|
|
} else {
|
|
assert.Error(t, err, "expected %q to be invalid", tc.pw)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatValidationErrors_AllTags(t *testing.T) {
|
|
cv := NewCustomValidator()
|
|
|
|
type allTags struct {
|
|
Required string `json:"required" validate:"required"`
|
|
Email string `json:"email" validate:"email"`
|
|
MinLen string `json:"min_len" validate:"min=5"`
|
|
MaxLen string `json:"max_len" validate:"max=3"`
|
|
OneOf string `json:"one_of" validate:"oneof=a b c"`
|
|
URL string `json:"url" validate:"url"`
|
|
}
|
|
|
|
input := allTags{
|
|
Required: "", // fails required
|
|
Email: "bad", // fails email
|
|
MinLen: "ab", // fails min=5
|
|
MaxLen: "abcde", // fails max=3
|
|
OneOf: "z", // fails oneof
|
|
URL: "nope", // fails url
|
|
}
|
|
|
|
err := cv.Validate(input)
|
|
require.Error(t, err)
|
|
|
|
resp := FormatValidationErrors(err)
|
|
require.NotNil(t, resp)
|
|
assert.Equal(t, "Validation failed", resp.Error)
|
|
|
|
expectedMessages := map[string]string{
|
|
"required": "This field is required",
|
|
"email": "Must be a valid email address",
|
|
"min_len": "Must be at least 5 characters",
|
|
"max_len": "Must be at most 3 characters",
|
|
"one_of": "Must be one of: a b c",
|
|
"url": "Must be a valid URL",
|
|
}
|
|
|
|
for field, expectedMsg := range expectedMessages {
|
|
fe, ok := resp.Fields[field]
|
|
assert.True(t, ok, "expected field %q in error response", field)
|
|
if ok {
|
|
assert.Equal(t, expectedMsg, fe.Message, "message mismatch for field %q", field)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFormatValidationErrors_NonValidationError(t *testing.T) {
|
|
err := fmt.Errorf("some random error")
|
|
resp := FormatValidationErrors(err)
|
|
require.NotNil(t, resp)
|
|
assert.Equal(t, "some random error", resp.Error)
|
|
assert.Nil(t, resp.Fields)
|
|
}
|
|
|
|
func TestNewCustomValidator_UsesJSONTagNames(t *testing.T) {
|
|
cv := NewCustomValidator()
|
|
|
|
type request struct {
|
|
FirstName string `json:"first_name" validate:"required"`
|
|
}
|
|
|
|
err := cv.Validate(request{})
|
|
require.Error(t, err)
|
|
|
|
resp := FormatValidationErrors(err)
|
|
require.NotNil(t, resp)
|
|
_, ok := resp.Fields["first_name"]
|
|
assert.True(t, ok, "expected JSON tag name 'first_name' in error fields")
|
|
}
|
|
|
|
func TestCustomValidator_Validate_Success(t *testing.T) {
|
|
cv := NewCustomValidator()
|
|
|
|
type request struct {
|
|
Name string `json:"name" validate:"required"`
|
|
}
|
|
|
|
err := cv.Validate(request{Name: "test"})
|
|
assert.NoError(t, err)
|
|
}
|