362 lines
9.6 KiB
Go
362 lines
9.6 KiB
Go
package accountapiimp
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/tech/sendico/pkg/model"
|
|
"github.com/stretchr/testify/assert"
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
)
|
|
|
|
// TestPasswordResetTokenGeneration tests the token generation logic
|
|
func TestPasswordResetTokenGeneration(t *testing.T) {
|
|
// Test that ResetPassword service method generates a token
|
|
account := &model.Account{
|
|
AccountPublic: model.AccountPublic{
|
|
AccountBase: model.AccountBase{
|
|
Describable: model.Describable{
|
|
Name: "Test User",
|
|
},
|
|
},
|
|
UserDataBase: model.UserDataBase{
|
|
Login: "test@example.com",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Initially no reset token
|
|
assert.Empty(t, account.ResetPasswordToken, "Account should not have reset token initially")
|
|
|
|
// Simulate what ResetPassword service method does
|
|
account.ResetPasswordToken = "generated-token-123"
|
|
assert.NotEmpty(t, account.ResetPasswordToken, "Reset token should be generated")
|
|
assert.Equal(t, "generated-token-123", account.ResetPasswordToken, "Reset token should match generated value")
|
|
}
|
|
|
|
// TestPasswordResetTokenValidation tests token validation logic
|
|
func TestPasswordResetTokenValidation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
storedToken string
|
|
providedToken string
|
|
shouldBeValid bool
|
|
}{
|
|
{
|
|
name: "ValidToken_ShouldMatch",
|
|
storedToken: "valid-token-123",
|
|
providedToken: "valid-token-123",
|
|
shouldBeValid: true,
|
|
},
|
|
{
|
|
name: "InvalidToken_ShouldNotMatch",
|
|
storedToken: "valid-token-123",
|
|
providedToken: "invalid-token-456",
|
|
shouldBeValid: false,
|
|
},
|
|
{
|
|
name: "EmptyStoredToken_ShouldBeInvalid",
|
|
storedToken: "",
|
|
providedToken: "any-token",
|
|
shouldBeValid: false,
|
|
},
|
|
{
|
|
name: "EmptyProvidedToken_ShouldBeInvalid",
|
|
storedToken: "valid-token-123",
|
|
providedToken: "",
|
|
shouldBeValid: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
account := &model.Account{
|
|
AccountPublic: model.AccountPublic{
|
|
AccountBase: model.AccountBase{
|
|
Describable: model.Describable{
|
|
Name: "Test User",
|
|
},
|
|
},
|
|
UserDataBase: model.UserDataBase{
|
|
Login: "test@example.com",
|
|
},
|
|
},
|
|
ResetPasswordToken: tt.storedToken,
|
|
}
|
|
|
|
// Test token validation logic (what the resetPassword handler does)
|
|
isValid := account.ResetPasswordToken != "" && account.ResetPasswordToken == tt.providedToken
|
|
assert.Equal(t, tt.shouldBeValid, isValid, "Token validation should match expected result")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPasswordResetFlowLogic tests the logical flow without database dependencies
|
|
func TestPasswordResetFlowLogic(t *testing.T) {
|
|
t.Run("CompleteFlow", func(t *testing.T) {
|
|
// Step 1: User requests password reset
|
|
userEmail := "test@example.com"
|
|
assert.NotEmpty(t, userEmail, "Email should not be empty")
|
|
assert.Contains(t, userEmail, "@", "Email should contain @ symbol")
|
|
|
|
// Step 2: System generates reset token
|
|
originalToken := ""
|
|
resetToken := "generated-reset-token-123"
|
|
assert.NotEmpty(t, resetToken, "Reset token should be generated")
|
|
assert.NotEqual(t, originalToken, resetToken, "Reset token should be different from empty")
|
|
|
|
// Step 3: User clicks reset link with token
|
|
userID := primitive.NewObjectID()
|
|
assert.NotEqual(t, primitive.NilObjectID, userID, "User ID should be valid")
|
|
|
|
// Step 4: System validates token and updates password
|
|
storedToken := resetToken
|
|
providedToken := resetToken
|
|
tokenValid := storedToken == providedToken
|
|
assert.True(t, tokenValid, "Token should be valid")
|
|
|
|
// Step 5: Password gets updated and token cleared
|
|
oldPassword := "old-password"
|
|
newPassword := "new-password-123!"
|
|
clearedToken := ""
|
|
|
|
assert.NotEqual(t, oldPassword, newPassword, "Password should be changed")
|
|
assert.Empty(t, clearedToken, "Token should be cleared after use")
|
|
assert.NotEqual(t, storedToken, clearedToken, "Token should be different after clearing")
|
|
})
|
|
|
|
t.Run("TokenSecurity", func(t *testing.T) {
|
|
// Test that tokens are single-use
|
|
originalToken := "valid-token-123"
|
|
usedToken := "" // After use, token should be cleared
|
|
|
|
assert.NotEmpty(t, originalToken, "Original token should exist")
|
|
assert.Empty(t, usedToken, "Used token should be cleared")
|
|
assert.NotEqual(t, originalToken, usedToken, "Token should be cleared after use")
|
|
|
|
// Test that different tokens are not equal
|
|
token1 := "token-123"
|
|
token2 := "token-456"
|
|
assert.NotEqual(t, token1, token2, "Different tokens should not be equal")
|
|
})
|
|
}
|
|
|
|
// TestPasswordValidationLogic tests password complexity requirements
|
|
func TestPasswordValidationLogic(t *testing.T) {
|
|
t.Run("ValidPasswords", func(t *testing.T) {
|
|
validPasswords := []string{
|
|
"Password123!",
|
|
"MySecurePass1@",
|
|
"ComplexP@ssw0rd",
|
|
}
|
|
|
|
for _, password := range validPasswords {
|
|
t.Run(password, func(t *testing.T) {
|
|
// Test minimum length
|
|
assert.True(t, len(password) >= 8, "Password should be at least 8 characters")
|
|
|
|
// Test for at least one digit
|
|
hasDigit := false
|
|
for _, char := range password {
|
|
if char >= '0' && char <= '9' {
|
|
hasDigit = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasDigit, "Password should contain at least one digit")
|
|
|
|
// Test for at least one uppercase letter
|
|
hasUpper := false
|
|
for _, char := range password {
|
|
if char >= 'A' && char <= 'Z' {
|
|
hasUpper = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasUpper, "Password should contain at least one uppercase letter")
|
|
|
|
// Test for at least one lowercase letter
|
|
hasLower := false
|
|
for _, char := range password {
|
|
if char >= 'a' && char <= 'z' {
|
|
hasLower = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasLower, "Password should contain at least one lowercase letter")
|
|
|
|
// Test for at least one special character
|
|
hasSpecial := false
|
|
specialChars := "!@#$%^&*()_+-=[]{}|;:,.<>?"
|
|
for _, char := range password {
|
|
for _, special := range specialChars {
|
|
if char == special {
|
|
hasSpecial = true
|
|
break
|
|
}
|
|
}
|
|
if hasSpecial {
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasSpecial, "Password should contain at least one special character")
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("InvalidPasswords", func(t *testing.T) {
|
|
invalidPasswords := []string{
|
|
"", // Empty
|
|
"short", // Too short
|
|
"nouppercase1!", // No uppercase
|
|
"NOLOWERCASE1!", // No lowercase
|
|
"NoNumbers!", // No numbers
|
|
"NoSpecial1", // No special characters
|
|
}
|
|
|
|
for _, password := range invalidPasswords {
|
|
t.Run(password, func(t *testing.T) {
|
|
// Test that invalid passwords fail at least one requirement
|
|
isValid := true
|
|
|
|
// Check length
|
|
if len(password) < 8 {
|
|
isValid = false
|
|
}
|
|
|
|
// Check for digit
|
|
hasDigit := false
|
|
for _, char := range password {
|
|
if char >= '0' && char <= '9' {
|
|
hasDigit = true
|
|
break
|
|
}
|
|
}
|
|
if !hasDigit {
|
|
isValid = false
|
|
}
|
|
|
|
// Check for uppercase
|
|
hasUpper := false
|
|
for _, char := range password {
|
|
if char >= 'A' && char <= 'Z' {
|
|
hasUpper = true
|
|
break
|
|
}
|
|
}
|
|
if !hasUpper {
|
|
isValid = false
|
|
}
|
|
|
|
// Check for lowercase
|
|
hasLower := false
|
|
for _, char := range password {
|
|
if char >= 'a' && char <= 'z' {
|
|
hasLower = true
|
|
break
|
|
}
|
|
}
|
|
if !hasLower {
|
|
isValid = false
|
|
}
|
|
|
|
// Check for special character
|
|
hasSpecial := false
|
|
specialChars := "!@#$%^&*()_+-=[]{}|;:,.<>?"
|
|
for _, char := range password {
|
|
for _, special := range specialChars {
|
|
if char == special {
|
|
hasSpecial = true
|
|
break
|
|
}
|
|
}
|
|
if hasSpecial {
|
|
break
|
|
}
|
|
}
|
|
if !hasSpecial {
|
|
isValid = false
|
|
}
|
|
|
|
assert.False(t, isValid, "Invalid password should fail validation")
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestEmailValidationLogic tests email format validation
|
|
func TestEmailValidationLogic(t *testing.T) {
|
|
t.Run("ValidEmails", func(t *testing.T) {
|
|
validEmails := []string{
|
|
"test@example.com",
|
|
"user.name@domain.org",
|
|
"user+tag@example.co.uk",
|
|
"test123@domain.com",
|
|
}
|
|
|
|
for _, email := range validEmails {
|
|
t.Run(email, func(t *testing.T) {
|
|
// Basic email validation logic
|
|
hasAt := false
|
|
hasDot := false
|
|
atIndex := -1
|
|
dotIndex := -1
|
|
|
|
for i, char := range email {
|
|
if char == '@' {
|
|
hasAt = true
|
|
atIndex = i
|
|
}
|
|
if char == '.' {
|
|
hasDot = true
|
|
dotIndex = i
|
|
}
|
|
}
|
|
|
|
assert.True(t, hasAt, "Valid email should contain @")
|
|
assert.True(t, hasDot, "Valid email should contain .")
|
|
assert.True(t, atIndex > 0, "Valid email should have @ not at start")
|
|
assert.True(t, dotIndex > atIndex, "Valid email should have . after @")
|
|
assert.True(t, len(email) > atIndex+1, "Valid email should have domain after @")
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("InvalidEmails", func(t *testing.T) {
|
|
invalidEmails := []string{
|
|
"", // Empty
|
|
"noat.com", // No @
|
|
"test@nodot", // No .
|
|
"@nodomain.com", // No local part
|
|
"test@.com", // No domain
|
|
"test.com@", // No domain after @
|
|
}
|
|
|
|
for _, email := range invalidEmails {
|
|
t.Run(email, func(t *testing.T) {
|
|
// Basic email validation logic
|
|
hasAt := false
|
|
hasDot := false
|
|
atIndex := -1
|
|
dotIndex := -1
|
|
|
|
for i, char := range email {
|
|
if char == '@' {
|
|
hasAt = true
|
|
atIndex = i
|
|
}
|
|
if char == '.' {
|
|
hasDot = true
|
|
dotIndex = i
|
|
}
|
|
}
|
|
|
|
// Invalid emails should fail at least one requirement
|
|
domainAfterDot := len(email) > dotIndex+1
|
|
domainAfterAt := len(email) > atIndex+1
|
|
isValid := hasAt && hasDot && atIndex > 0 && dotIndex > atIndex && domainAfterAt && domainAfterDot && (dotIndex-atIndex) > 1
|
|
assert.False(t, isValid, "Invalid email should fail validation")
|
|
})
|
|
}
|
|
})
|
|
}
|