package service import ( "fmt" "math/rand" "net/http" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" passwordvalidator "github.com/wagslane/go-password-validator" ) type MockHTTPClient struct { mock.Mock } func (m *MockHTTPClient) Get(url string) (*http.Response, error) { args := m.Called(url) return args.Get(0).(*http.Response), args.Error(1) } func TestValidatePassword(t *testing.T) { tests := []struct { name string password string wantErr string mockHTTP func(*MockHTTPClient) }{ { name: "too short", password: strings.Repeat("a", minPasswordLength-1), wantErr: fmt.Sprintf("password must be at least %d characters", minPasswordLength), }, { name: "too long", password: strings.Repeat("a", maxPasswordLength+1), wantErr: fmt.Sprintf("password cannot be longer than %d characters", maxPasswordLength), }, { name: "low entropy", password: strings.Repeat("a", minPasswordLength), wantErr: "insecure password", // Error produced by wagslane/go-password-validator }, { name: "valid password", password: "SecurePassw0rd!123", wantErr: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Mock HTTP client if needed if tt.mockHTTP != nil { mockClient := new(MockHTTPClient) tt.mockHTTP(mockClient) } err := validatePassword(tt.password) if tt.wantErr == "" { assert.NoError(t, err) } else { assert.ErrorContains(t, err, tt.wantErr) } }) } } func TestIsPasswordCompromised(t *testing.T) { t.Run("known compromised", func(t *testing.T) { compromised, err := isPasswordCompromised("password123456") assert.NoError(t, err) assert.True(t, compromised) }) t.Run("randomly generated", func(t *testing.T) { randomStr := genRandomString(t, 12) compromised, err := isPasswordCompromised(randomStr) assert.NoError(t, err) assert.False(t, compromised) }) } func TestPasswordEntropyCalculation(t *testing.T) { tests := []struct { password string entropy float64 }{ {"password", 37.6}, {"SecurePassw0rd!123", 103.12}, {"aaaaaaaaaaaaaaaa", 9.5}, } for _, tt := range tests { t.Run(tt.password, func(t *testing.T) { entropy := passwordvalidator.GetEntropy(tt.password) assert.InDelta(t, tt.entropy, entropy, 1.0) }) } } func TestValidateUsername(t *testing.T) { tests := []struct { name string input string wantErr string }{ { name: "too short", input: strings.Repeat("a", minUsernameLength-1), wantErr: fmt.Sprintf("username must be at least %d characters", minUsernameLength), }, { name: "too long", input: strings.Repeat("a", maxUsernameLength+1), wantErr: fmt.Sprintf("username cannot be longer than %d characters", maxUsernameLength), }, { name: "invalid characters", input: "user@name", wantErr: "username can only contain numbers, letters, and underscores", }, { name: "valid username", input: "valid_user123", wantErr: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateUsername(tt.input) if tt.wantErr == "" { assert.NoError(t, err) } else { assert.ErrorContains(t, err, tt.wantErr) } }) } } func TestNormalizeUsername(t *testing.T) { tests := []struct { name string input string want string }{ { name: "trim whitespace", input: " test_user ", want: "test_user", }, { name: "lowercase", input: "TestUser", want: "testuser", }, { name: "mixed case and spaces", input: " UserName123 ", want: "username123", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := normalizeUsername(tt.input) assert.Equal(t, tt.want, got) }) } } func genRandomString(t *testing.T, length int) string { t.Helper() const charset = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) b := make([]byte, length) for i := range b { b[i] = charset[seededRand.Intn(len(charset))] } return string(b) }