yao/openapi/oauth/providers/user/utils_test.go
Max 7c332c6811 Add go-nanoid dependency and refactor user provider methods
- Added the go-nanoid library for generating unique IDs, enhancing user ID management.
- Refactored user provider methods to improve clarity and consistency, including updates to user retrieval and authentication processes.
- Adjusted user model fields to align with new ID generation strategy, ensuring compliance with best practices.
- Cleaned up code by removing obsolete test files and improving overall structure for better maintainability.
2025-08-02 17:49:03 +08:00

356 lines
10 KiB
Go

package user_test
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yaoapp/kun/maps"
"github.com/yaoapp/yao/openapi/oauth/providers/user"
)
func TestGenerateUserID(t *testing.T) {
prepare(t)
defer clean()
ctx := context.Background()
t.Run("NanoID_Strategy_Safe_Mode", func(t *testing.T) {
// Test NanoID strategy with safe mode (default)
provider := user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: "test:",
IDStrategy: user.NanoIDStrategy,
IDPrefix: "user_",
})
userID, err := provider.GenerateUserID(ctx, true)
assert.NoError(t, err)
assert.NotEmpty(t, userID)
assert.True(t, strings.HasPrefix(userID, "user_"))
assert.Greater(t, len(userID), 5) // Should be "user_" + at least some characters
})
t.Run("NanoID_Strategy_Unsafe_Mode", func(t *testing.T) {
provider := user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: "test:",
IDStrategy: user.NanoIDStrategy,
IDPrefix: "test_",
})
userID, err := provider.GenerateUserID(ctx, false)
assert.NoError(t, err)
assert.NotEmpty(t, userID)
assert.True(t, strings.HasPrefix(userID, "test_"))
})
t.Run("UUID_Strategy_Safe_Mode", func(t *testing.T) {
provider := user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: "test:",
IDStrategy: user.UUIDStrategy,
IDPrefix: "uuid_",
})
userID, err := provider.GenerateUserID(ctx, true)
assert.NoError(t, err)
assert.NotEmpty(t, userID)
assert.True(t, strings.HasPrefix(userID, "uuid_"))
// UUID format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx (36 chars) + prefix
assert.Greater(t, len(userID), 40)
})
t.Run("UUID_Strategy_Unsafe_Mode", func(t *testing.T) {
provider := user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: "test:",
IDStrategy: user.UUIDStrategy,
IDPrefix: "",
})
userID, err := provider.GenerateUserID(ctx, false)
assert.NoError(t, err)
assert.NotEmpty(t, userID)
assert.Len(t, userID, 36) // Standard UUID length without prefix
})
t.Run("Default_Safe_Mode_Behavior", func(t *testing.T) {
// NanoID should default to safe mode
providerNano := user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: "test:",
IDStrategy: user.NanoIDStrategy,
})
userID, err := providerNano.GenerateUserID(ctx)
assert.NoError(t, err)
assert.NotEmpty(t, userID)
// UUID should default to unsafe mode
providerUUID := user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: "test:",
IDStrategy: user.UUIDStrategy,
})
userID2, err := providerUUID.GenerateUserID(ctx)
assert.NoError(t, err)
assert.NotEmpty(t, userID2)
assert.Len(t, userID2, 36) // Standard UUID length
})
t.Run("Collision_Detection", func(t *testing.T) {
provider := user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: "test:",
IDStrategy: user.NanoIDStrategy,
IDPrefix: "collision_",
})
// Generate first ID and create user with it
userID1, err := provider.GenerateUserID(ctx, true)
require.NoError(t, err)
userData := maps.MapStrAny{
"user_id": userID1,
"preferred_username": "collisiontest",
"email": "collision@test.com",
"password": "TestPass123!",
"status": "active",
}
_, err = provider.CreateUser(ctx, userData)
require.NoError(t, err)
// Generate second ID - should be different due to collision detection
userID2, err := provider.GenerateUserID(ctx, true)
assert.NoError(t, err)
assert.NotEqual(t, userID1, userID2)
assert.True(t, strings.HasPrefix(userID2, "collision_"))
// Clean up
provider.DeleteUser(ctx, userID1)
})
t.Run("Multiple_IDs_Uniqueness", func(t *testing.T) {
provider := user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: "test:",
IDStrategy: user.NanoIDStrategy,
})
ids := make(map[string]bool)
for i := 0; i < 100; i++ {
userID, err := provider.GenerateUserID(ctx, false)
assert.NoError(t, err)
assert.NotEmpty(t, userID)
// Check uniqueness
assert.False(t, ids[userID], "Generated duplicate ID: %s", userID)
ids[userID] = true
}
})
}
// TODO: TestGetOAuthUserID - depends on CreateOAuthAccount implementation
// func TestGetOAuthUserID(t *testing.T) {
// // Will be implemented after CreateOAuthAccount is implemented
// }
func TestNanoIDGeneration(t *testing.T) {
t.Run("NanoID_Length_And_Characters", func(t *testing.T) {
provider := user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: "test:",
IDStrategy: user.NanoIDStrategy,
})
// Generate multiple NanoIDs and check their properties
for i := 0; i < 10; i++ {
userID, err := provider.GenerateUserID(context.Background(), false)
assert.NoError(t, err)
assert.NotEmpty(t, userID)
// NanoID should be 12 characters (default length)
assert.Len(t, userID, 12)
// Should only contain allowed characters (no ambiguous chars like 0, O, 1, l, I)
allowedChars := "23456789ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz"
for _, char := range userID {
assert.Contains(t, allowedChars, string(char), "Invalid character in NanoID: %c", char)
}
// Should not contain ambiguous characters
forbiddenChars := "01OIl"
for _, char := range forbiddenChars {
assert.NotContains(t, userID, string(char), "NanoID contains ambiguous character: %c", char)
}
}
})
t.Run("NanoID_With_Prefix", func(t *testing.T) {
provider := user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: "test:",
IDStrategy: user.NanoIDStrategy,
IDPrefix: "nano_",
})
userID, err := provider.GenerateUserID(context.Background(), false)
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(userID, "nano_"))
assert.Len(t, userID, 17) // "nano_" (5) + 12 chars
})
}
func TestUUIDGeneration(t *testing.T) {
t.Run("UUID_Format", func(t *testing.T) {
provider := user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: "test:",
IDStrategy: user.UUIDStrategy,
})
// Generate multiple UUIDs and check their format
for i := 0; i < 10; i++ {
userID, err := provider.GenerateUserID(context.Background(), false)
assert.NoError(t, err)
assert.NotEmpty(t, userID)
// UUID format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
assert.Len(t, userID, 36)
assert.Equal(t, byte('-'), userID[8])
assert.Equal(t, byte('-'), userID[13])
assert.Equal(t, byte('-'), userID[18])
assert.Equal(t, byte('-'), userID[23])
// Should be version 4 UUID (14th character should be '4')
assert.Equal(t, byte('4'), userID[14])
// 19th character should be one of '8', '9', 'a', 'b' (variant bits)
variant := userID[19]
assert.True(t, variant == '8' || variant == '9' || variant == 'a' || variant == 'b',
"Invalid UUID variant: %c", variant)
}
})
t.Run("UUID_With_Prefix", func(t *testing.T) {
provider := user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: "test:",
IDStrategy: user.UUIDStrategy,
IDPrefix: "uuid_",
})
userID, err := provider.GenerateUserID(context.Background(), false)
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(userID, "uuid_"))
assert.Len(t, userID, 41) // "uuid_" (5) + 36 chars
})
}
func TestRandomPasswordGeneration(t *testing.T) {
prepare(t)
defer clean()
ctx := context.Background()
t.Run("ResetPassword_Generates_Random_Password", func(t *testing.T) {
// Create test user
testUserData := createTestUserData("password")
_, testUserID := setupTestUser(t, ctx, testUserData)
// Reset password should generate a random 12-character password
randomPassword, err := testProvider.ResetPassword(ctx, testUserID)
assert.NoError(t, err)
assert.NotEmpty(t, randomPassword)
assert.Len(t, randomPassword, 12)
// Password should contain mix of characters
hasUpper := false
hasLower := false
hasDigit := false
hasSpecial := false
for _, char := range randomPassword {
switch {
case char >= 'A' && char <= 'Z':
hasUpper = true
case char >= 'a' && char <= 'z':
hasLower = true
case char >= '0' && char <= '9':
hasDigit = true
case strings.ContainsRune("!@#$%^&*", char):
hasSpecial = true
}
}
// Should have at least some variety (not enforcing all types for 12 chars)
varietyCount := 0
if hasUpper {
varietyCount++
}
if hasLower {
varietyCount++
}
if hasDigit {
varietyCount++
}
if hasSpecial {
varietyCount++
}
assert.Greater(t, varietyCount, 1, "Password should have variety in character types")
// Clean up
testProvider.DeleteUser(ctx, testUserID)
})
t.Run("Multiple_Random_Passwords_Are_Different", func(t *testing.T) {
// Create test user
testUserData := createTestUserData("multipass")
_, testUserID := setupTestUser(t, ctx, testUserData)
passwords := make(map[string]bool)
for i := 0; i < 10; i++ {
randomPassword, err := testProvider.ResetPassword(ctx, testUserID)
assert.NoError(t, err)
assert.NotEmpty(t, randomPassword)
// Check uniqueness
assert.False(t, passwords[randomPassword], "Generated duplicate password: %s", randomPassword)
passwords[randomPassword] = true
}
// Clean up
testProvider.DeleteUser(ctx, testUserID)
})
}
func TestIDStrategyConfiguration(t *testing.T) {
t.Run("Default_Strategy_Is_NanoID", func(t *testing.T) {
provider := user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: "test:",
// No IDStrategy specified, should default to NanoID
})
userID, err := provider.GenerateUserID(context.Background(), false)
assert.NoError(t, err)
assert.NotEmpty(t, userID)
assert.Len(t, userID, 12) // NanoID length
})
t.Run("Explicit_NanoID_Strategy", func(t *testing.T) {
provider := user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: "test:",
IDStrategy: user.NanoIDStrategy,
})
userID, err := provider.GenerateUserID(context.Background(), false)
assert.NoError(t, err)
assert.NotEmpty(t, userID)
assert.Len(t, userID, 12)
})
t.Run("Explicit_UUID_Strategy", func(t *testing.T) {
provider := user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: "test:",
IDStrategy: user.UUIDStrategy,
})
userID, err := provider.GenerateUserID(context.Background(), false)
assert.NoError(t, err)
assert.NotEmpty(t, userID)
assert.Len(t, userID, 36)
})
}