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 _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Mock HTTP client if needed if tc.mockHTTP != nil { mockClient := new(MockHTTPClient) tc.mockHTTP(mockClient) } err := validatePassword(tc.password) if tc.wantErr == "" { assert.NoError(t, err) } else { assert.ErrorContains(t, err, tc.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 _, tc := range tests { t.Run(tc.password, func(t *testing.T) { entropy := passwordvalidator.GetEntropy(tc.password) assert.InDelta(t, tc.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 _, tc := range tests { t.Run(tc.name, func(t *testing.T) { err := validateUsername(tc.input) if tc.wantErr == "" { assert.NoError(t, err) } else { assert.ErrorContains(t, err, tc.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 _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got := normalizeUsername(tc.input) assert.Equal(t, tc.want, got) }) } } func TestGetPaginationParams(t *testing.T) { tests := []struct { name string query string expectedLimit int32 expectedOffset int32 }{ { name: "defaults", query: "", expectedLimit: 50, expectedOffset: 0, }, { name: "valid limit and offset", query: "limit=25&offset=30", expectedLimit: 25, expectedOffset: 30, }, { name: "invalid limit", query: "limit=abc&offset=5", expectedLimit: 50, expectedOffset: 5, }, { name: "limit zero", query: "limit=0", expectedLimit: 50, expectedOffset: 0, }, { name: "limit negative", query: "limit=-5", expectedLimit: 50, expectedOffset: 0, }, { name: "offset negative", query: "offset=-10", expectedLimit: 50, expectedOffset: 0, }, { name: "invalid offset", query: "offset=xyz", expectedLimit: 50, expectedOffset: 0, }, { name: "valid offset zero", query: "offset=0", expectedLimit: 50, expectedOffset: 0, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { req, err := http.NewRequest("GET", "/test?"+tc.query, nil) assert.Nil(t, err) limit, offset := getPaginationParams(req) assert.Equal(t, tc.expectedLimit, limit) assert.Equal(t, tc.expectedOffset, offset) }) } } func TestSHA1ContentHash(t *testing.T) { tests := []struct { name string title string content string expected string }{ { name: "empty strings", title: "", content: "", expected: "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", }, { name: "title only", title: "hello", content: "", expected: "AAF4C61DDCC5E8A2DABEDE0F3B482CD9AEA9434D", }, { name: "content only", title: "", content: "world", expected: "7C211433F02071597741E6FF5A8EA34789ABBF43", }, { name: "both title and content", title: "hello", content: "world", expected: "6ADFB183A4A2C94A2F92DAB5ADE762A47889A5A1", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { result := sha1ContentHash(tc.title, tc.content) assert.Equal(t, tc.expected, result) }) } } 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) }