304 lines
6.2 KiB
Go
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)
|
|
}
|