304 lines
6.2 KiB
Go

package service
import (
"fmt"
"math/rand"
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
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: "password is too weak",
},
{
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",
24,
},
{
"SecurePassw0rd!123",
73,
},
{
"aaaaaaaaaaaaaaaa",
5,
},
}
for _, tc := range tests {
t.Run(tc.password, func(t *testing.T) {
entropy := calcPasswordEntropy(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)
}