- Modified the UpdateUserLastLogin method to validate that loginCtx is not nil, returning an error if it is. - Updated the corresponding test to reflect this change, ensuring that an error is asserted when loginCtx is nil, improving error handling and robustness of user login tracking.
326 lines
10 KiB
Go
326 lines
10 KiB
Go
package user_test
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/yaoapp/kun/maps"
|
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
|
)
|
|
|
|
func TestUserBasicOperations(t *testing.T) {
|
|
prepare(t)
|
|
defer clean()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Use UUID to ensure unique identifiers
|
|
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8] // 8 char UUID
|
|
|
|
// Create test user data dynamically
|
|
testUser := &TestUserData{
|
|
PreferredUsername: "testuser" + testUUID,
|
|
Email: "testuser" + testUUID + "@example.com",
|
|
Password: "TestPass123!",
|
|
Name: "Test User " + testUUID,
|
|
GivenName: "Test",
|
|
FamilyName: "User",
|
|
Status: "active",
|
|
RoleID: "user",
|
|
TypeID: "regular",
|
|
EmailVerified: true,
|
|
Metadata: map[string]interface{}{"source": "test"},
|
|
}
|
|
|
|
var testUserID string // Store the auto-generated user_id
|
|
|
|
// Test CreateUser
|
|
t.Run("CreateUser", func(t *testing.T) {
|
|
userMap := maps.MapStrAny{
|
|
"preferred_username": testUser.PreferredUsername,
|
|
"email": testUser.Email,
|
|
"password": testUser.Password,
|
|
"name": testUser.Name,
|
|
"given_name": testUser.GivenName,
|
|
"family_name": testUser.FamilyName,
|
|
"status": testUser.Status,
|
|
"role_id": testUser.RoleID,
|
|
"type_id": testUser.TypeID,
|
|
"email_verified": testUser.EmailVerified,
|
|
"metadata": testUser.Metadata,
|
|
}
|
|
|
|
id, err := testProvider.CreateUser(ctx, userMap)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, id)
|
|
|
|
// Verify user was created with auto-generated user_id
|
|
assert.Contains(t, userMap, "user_id")
|
|
assert.NotEmpty(t, userMap["user_id"])
|
|
|
|
// Store generated user_id for subsequent tests
|
|
testUserID = userMap["user_id"].(string)
|
|
})
|
|
|
|
// Test GetUser
|
|
t.Run("GetUser", func(t *testing.T) {
|
|
user, err := testProvider.GetUser(ctx, testUserID)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, user)
|
|
assert.Equal(t, testUser.PreferredUsername, user["preferred_username"])
|
|
assert.Equal(t, testUser.Email, user["email"])
|
|
assert.Equal(t, testUser.Name, user["name"])
|
|
|
|
// Should not contain password_hash in public fields
|
|
assert.NotContains(t, user, "password_hash")
|
|
})
|
|
|
|
// Test GetUserByPreferredUsername
|
|
t.Run("GetUserByPreferredUsername", func(t *testing.T) {
|
|
user, err := testProvider.GetUserByPreferredUsername(ctx, testUser.PreferredUsername)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, user)
|
|
assert.Equal(t, testUserID, user["user_id"])
|
|
assert.Equal(t, testUser.Email, user["email"])
|
|
})
|
|
|
|
// Test GetUserByEmail
|
|
t.Run("GetUserByEmail", func(t *testing.T) {
|
|
user, err := testProvider.GetUserByEmail(ctx, testUser.Email)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, user)
|
|
assert.Equal(t, testUserID, user["user_id"])
|
|
assert.Equal(t, testUser.PreferredUsername, user["preferred_username"])
|
|
})
|
|
|
|
// Test GetUserForAuth
|
|
t.Run("GetUserForAuth", func(t *testing.T) {
|
|
user, err := testProvider.GetUserForAuth(ctx, testUserID, "user_id")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, user)
|
|
assert.Equal(t, testUser.PreferredUsername, user["preferred_username"])
|
|
|
|
// Should contain password_hash for auth
|
|
assert.Contains(t, user, "password_hash")
|
|
assert.NotEmpty(t, user["password_hash"])
|
|
})
|
|
|
|
// Test VerifyPassword
|
|
t.Run("VerifyPassword", func(t *testing.T) {
|
|
// Get user auth data first
|
|
user, err := testProvider.GetUserForAuth(ctx, testUserID, "user_id")
|
|
assert.NoError(t, err)
|
|
|
|
passwordHash := user["password_hash"].(string)
|
|
|
|
// Test correct password
|
|
valid, err := testProvider.VerifyPassword(ctx, testUser.Password, passwordHash)
|
|
assert.NoError(t, err)
|
|
assert.True(t, valid)
|
|
|
|
// Test incorrect password
|
|
valid, err = testProvider.VerifyPassword(ctx, "wrongpassword", passwordHash)
|
|
assert.NoError(t, err)
|
|
assert.False(t, valid)
|
|
|
|
// Test empty password hash
|
|
valid, err = testProvider.VerifyPassword(ctx, testUser.Password, "")
|
|
assert.Error(t, err)
|
|
assert.False(t, valid)
|
|
assert.Contains(t, err.Error(), "no password hash found")
|
|
})
|
|
|
|
// Test UpdateUser
|
|
t.Run("UpdateUser", func(t *testing.T) {
|
|
updateData := maps.MapStrAny{
|
|
"name": "Updated Test User",
|
|
"given_name": "Updated",
|
|
"family_name": "User",
|
|
"metadata": map[string]interface{}{"updated": true},
|
|
}
|
|
|
|
err := testProvider.UpdateUser(ctx, testUserID, updateData)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify update
|
|
user, err := testProvider.GetUser(ctx, testUserID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Updated Test User", user["name"])
|
|
assert.Equal(t, "Updated", user["given_name"])
|
|
|
|
// Test updating sensitive fields (should be ignored)
|
|
sensitiveData := maps.MapStrAny{
|
|
"password": "newpassword",
|
|
"password_hash": "newhash",
|
|
"mfa_secret": "newsecret",
|
|
}
|
|
|
|
err = testProvider.UpdateUser(ctx, testUserID, sensitiveData)
|
|
assert.NoError(t, err) // Should not error, just ignore sensitive fields
|
|
})
|
|
|
|
// Test UpdatePassword
|
|
t.Run("UpdatePassword", func(t *testing.T) {
|
|
newPassword := "NewTestPass789!"
|
|
|
|
err := testProvider.UpdatePassword(ctx, testUserID, newPassword)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify password was updated
|
|
user, err := testProvider.GetUserForAuth(ctx, testUserID, "user_id")
|
|
assert.NoError(t, err)
|
|
|
|
passwordHash := user["password_hash"].(string)
|
|
valid, err := testProvider.VerifyPassword(ctx, newPassword, passwordHash)
|
|
assert.NoError(t, err)
|
|
assert.True(t, valid)
|
|
|
|
// Old password should not work
|
|
valid, err = testProvider.VerifyPassword(ctx, testUser.Password, passwordHash)
|
|
assert.NoError(t, err)
|
|
assert.False(t, valid)
|
|
})
|
|
|
|
// Test ResetPassword
|
|
t.Run("ResetPassword", func(t *testing.T) {
|
|
randomPassword, err := testProvider.ResetPassword(ctx, testUserID)
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, randomPassword)
|
|
assert.Len(t, randomPassword, 12) // Should be 12 characters
|
|
|
|
// Verify random password works
|
|
user, err := testProvider.GetUserForAuth(ctx, testUserID, "user_id")
|
|
assert.NoError(t, err)
|
|
|
|
passwordHash := user["password_hash"].(string)
|
|
valid, err := testProvider.VerifyPassword(ctx, randomPassword, passwordHash)
|
|
assert.NoError(t, err)
|
|
assert.True(t, valid)
|
|
})
|
|
|
|
// Test UpdateUserLastLogin
|
|
t.Run("UpdateUserLastLogin", func(t *testing.T) {
|
|
loginCtx := &types.LoginContext{
|
|
IP: "127.0.0.1",
|
|
UserAgent: "Mozilla/5.0 (Test Browser)",
|
|
Device: "desktop",
|
|
Platform: "web",
|
|
}
|
|
err := testProvider.UpdateUserLastLogin(ctx, testUserID, loginCtx)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify last_login_at and context were updated
|
|
user, err := testProvider.GetUser(ctx, testUserID)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, user["last_login_at"])
|
|
assert.Equal(t, "127.0.0.1", user["last_login_ip"])
|
|
assert.Equal(t, "Mozilla/5.0 (Test Browser)", user["last_login_user_agent"])
|
|
assert.Equal(t, "desktop", user["last_login_device"])
|
|
assert.Equal(t, "web", user["last_login_platform"])
|
|
|
|
// Test with nil loginCtx (should return error)
|
|
err = testProvider.UpdateUserLastLogin(ctx, testUserID, nil)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "loginCtx is required")
|
|
|
|
// Test with partial loginCtx (only IP)
|
|
partialCtx := &types.LoginContext{
|
|
IP: "192.168.1.1",
|
|
}
|
|
err = testProvider.UpdateUserLastLogin(ctx, testUserID, partialCtx)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify IP was updated but other fields remain from previous login
|
|
user, err = testProvider.GetUser(ctx, testUserID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "192.168.1.1", user["last_login_ip"])
|
|
assert.Equal(t, "Mozilla/5.0 (Test Browser)", user["last_login_user_agent"])
|
|
})
|
|
|
|
// Test UpdateUserStatus
|
|
t.Run("UpdateUserStatus", func(t *testing.T) {
|
|
err := testProvider.UpdateUserStatus(ctx, testUserID, "suspended")
|
|
assert.NoError(t, err)
|
|
|
|
// Verify status was updated
|
|
user, err := testProvider.GetUser(ctx, testUserID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "suspended", user["status"])
|
|
})
|
|
|
|
// Test DeleteUser (at the end)
|
|
t.Run("DeleteUser", func(t *testing.T) {
|
|
err := testProvider.DeleteUser(ctx, testUserID)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify user was deleted
|
|
_, err = testProvider.GetUser(ctx, testUserID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "user not found")
|
|
})
|
|
}
|
|
|
|
func TestUserErrorHandling(t *testing.T) {
|
|
prepare(t)
|
|
defer clean()
|
|
|
|
ctx := context.Background()
|
|
// Use UUID to avoid conflicts
|
|
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
|
nonExistentUserID := "non-existent-user-id-" + testUUID
|
|
|
|
t.Run("GetUser_NotFound", func(t *testing.T) {
|
|
_, err := testProvider.GetUser(ctx, nonExistentUserID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "user not found")
|
|
})
|
|
|
|
t.Run("GetUserByPreferredUsername_NotFound", func(t *testing.T) {
|
|
_, err := testProvider.GetUserByPreferredUsername(ctx, "nonexistent")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "user not found")
|
|
})
|
|
|
|
t.Run("GetUserByEmail_NotFound", func(t *testing.T) {
|
|
_, err := testProvider.GetUserByEmail(ctx, "nonexistent@example.com")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "user not found")
|
|
})
|
|
|
|
t.Run("GetUserForAuth_InvalidIdentifierType", func(t *testing.T) {
|
|
_, err := testProvider.GetUserForAuth(ctx, "test", "invalid_type")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "invalid identifier type")
|
|
})
|
|
|
|
t.Run("UpdateUser_NotFound", func(t *testing.T) {
|
|
updateData := maps.MapStrAny{"name": "Test"}
|
|
err := testProvider.UpdateUser(ctx, nonExistentUserID, updateData)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "user not found")
|
|
})
|
|
|
|
t.Run("UpdatePassword_NotFound", func(t *testing.T) {
|
|
err := testProvider.UpdatePassword(ctx, nonExistentUserID, "newpassword")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "user not found")
|
|
})
|
|
|
|
t.Run("ResetPassword_NotFound", func(t *testing.T) {
|
|
_, err := testProvider.ResetPassword(ctx, nonExistentUserID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "user not found")
|
|
})
|
|
|
|
t.Run("DeleteUser_NotFound", func(t *testing.T) {
|
|
err := testProvider.DeleteUser(ctx, nonExistentUserID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "user not found")
|
|
})
|
|
}
|
|
|
|
// NOTE: TestIDGeneration moved to utils_test.go (tests utils.go methods)
|
|
// NOTE: TestFieldListConfiguration moved to default_test.go (tests configuration, not basic operations)
|