notatest/server/pkg/service/util_test.go

187 lines
4.1 KiB
Go

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)
}