Implement user type management methods and enhance user provider functionality

- Added methods for creating, retrieving, updating, and deleting user types, improving user type management capabilities.
- Introduced type field lists in DefaultUser and DefaultUserOptions for better type configuration.
- Implemented error handling for user type operations, ensuring robust feedback for failures.
- Enhanced user deletion process to clean up associated data before removing user accounts.
- Updated tests to cover new user type functionalities and ensure proper cleanup of test data.
This commit is contained in:
Max 2025-08-02 19:56:48 +08:00
parent 73846c9b9d
commit 5c96312eef
8 changed files with 1752 additions and 61 deletions

View file

@ -90,6 +90,19 @@ var (
"color", "icon", "max_users", "requires_approval", "auto_revoke_days",
"metadata", "conditions", "created_at", "updated_at",
}
// DefaultTypeFields contains basic type fields
DefaultTypeFields = []interface{}{
"id", "type_id", "name", "description", "is_active", "is_default", "sort_order",
"default_role_id", "max_sessions", "session_timeout", "created_at", "updated_at",
}
// DefaultTypeDetailFields contains all type fields including configuration and metadata
DefaultTypeDetailFields = []interface{}{
"id", "type_id", "name", "description", "default_role_id", "schema", "metadata",
"is_active", "is_default", "sort_order", "max_sessions", "session_timeout",
"password_policy", "features", "limits", "created_at", "updated_at",
}
)
// DefaultUser provides a default implementation of UserProvider
@ -118,6 +131,10 @@ type DefaultUser struct {
// Role Field lists
roleFields []interface{} // configurable
roleDetailFields []interface{} // configurable
// Type Field lists
typeFields []interface{} // configurable
typeDetailFields []interface{} // configurable
}
// IDStrategy defines the strategy for generating user IDs
@ -154,6 +171,10 @@ type DefaultUserOptions struct {
// Role field lists (use defaults if not specified)
RoleFields []interface{} // basic role fields
RoleDetailFields []interface{} // detailed role fields including permissions and metadata
// Type field lists (use defaults if not specified)
TypeFields []interface{} // basic type fields
TypeDetailFields []interface{} // detailed type fields including configuration and metadata
}
// NewDefaultUser creates a new DefaultUser
@ -221,6 +242,17 @@ func NewDefaultUser(options *DefaultUserOptions) *DefaultUser {
roleDetailFields = DefaultRoleDetailFields
}
// Set type field lists with defaults if not specified
typeFields := options.TypeFields
if typeFields == nil {
typeFields = DefaultTypeFields
}
typeDetailFields := options.TypeDetailFields
if typeDetailFields == nil {
typeDetailFields = DefaultTypeDetailFields
}
return &DefaultUser{
prefix: options.Prefix,
model: model,
@ -242,5 +274,9 @@ func NewDefaultUser(options *DefaultUserOptions) *DefaultUser {
// Role field lists
roleFields: roleFields,
roleDetailFields: roleDetailFields,
// Type field lists
typeFields: typeFields,
typeDetailFields: typeDetailFields,
}
}

View file

@ -2,6 +2,7 @@ package user
import (
"context"
"fmt"
"github.com/yaoapp/gou/model"
"github.com/yaoapp/kun/maps"
@ -11,54 +12,277 @@ import (
// GetType retrieves type information by type_id
func (u *DefaultUser) GetType(ctx context.Context, typeID string) (maps.MapStrAny, error) {
// TODO: implement
return nil, nil
m := model.Select(u.typeModel)
types, err := m.Get(model.QueryParam{
Select: u.typeFields,
Wheres: []model.QueryWhere{
{Column: "type_id", Value: typeID},
},
Limit: 1,
})
if err != nil {
return nil, fmt.Errorf(ErrFailedToGetType, err)
}
if len(types) == 0 {
return nil, fmt.Errorf(ErrTypeNotFound)
}
return types[0], nil
}
// CreateType creates a new user type
func (u *DefaultUser) CreateType(ctx context.Context, typeData maps.MapStrAny) (interface{}, error) {
// TODO: implement - type_id should be provided in typeData
return nil, nil
// Validate required type_id field
if _, exists := typeData["type_id"]; !exists {
return nil, fmt.Errorf("type_id is required in typeData")
}
// Set default values if not provided
if _, exists := typeData["is_active"]; !exists {
typeData["is_active"] = true
}
if _, exists := typeData["is_default"]; !exists {
typeData["is_default"] = false
}
if _, exists := typeData["sort_order"]; !exists {
typeData["sort_order"] = 0
}
if _, exists := typeData["max_sessions"]; !exists {
typeData["max_sessions"] = nil // Allow unlimited sessions by default
}
if _, exists := typeData["session_timeout"]; !exists {
typeData["session_timeout"] = 0 // No timeout by default
}
m := model.Select(u.typeModel)
id, err := m.Create(typeData)
if err != nil {
return nil, fmt.Errorf(ErrFailedToCreateType, err)
}
return id, nil
}
// UpdateType updates an existing type
func (u *DefaultUser) UpdateType(ctx context.Context, typeID string, typeData maps.MapStrAny) error {
// TODO: implement
// Remove sensitive fields that should not be updated directly
sensitiveFields := []string{"id", "type_id", "created_at"}
for _, field := range sensitiveFields {
delete(typeData, field)
}
// Skip update if no valid fields remain
if len(typeData) == 0 {
return nil
}
m := model.Select(u.typeModel)
affected, err := m.UpdateWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "type_id", Value: typeID},
},
Limit: 1, // Safety: ensure only one record is updated
}, typeData)
if err != nil {
return fmt.Errorf(ErrFailedToUpdateType, err)
}
if affected == 0 {
return fmt.Errorf(ErrTypeNotFound)
}
return nil
}
// DeleteType soft deletes a type
func (u *DefaultUser) DeleteType(ctx context.Context, typeID string) error {
// TODO: implement
// First check if type exists
m := model.Select(u.typeModel)
types, err := m.Get(model.QueryParam{
Select: []interface{}{"id", "type_id"},
Wheres: []model.QueryWhere{
{Column: "type_id", Value: typeID},
},
Limit: 1,
})
if err != nil {
return fmt.Errorf(ErrFailedToGetType, err)
}
if len(types) == 0 {
return fmt.Errorf(ErrTypeNotFound)
}
// Proceed with soft delete
affected, err := m.DeleteWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "type_id", Value: typeID},
},
Limit: 1, // Safety: ensure only one record is deleted
})
if err != nil {
return fmt.Errorf(ErrFailedToDeleteType, err)
}
if affected == 0 {
return fmt.Errorf(ErrTypeNotFound)
}
return nil
}
// GetTypes retrieves types by query parameters
func (u *DefaultUser) GetTypes(ctx context.Context, param model.QueryParam) ([]maps.MapStr, error) {
// TODO: implement
return nil, nil
// Set default select fields if not provided
if param.Select == nil {
param.Select = u.typeFields
}
m := model.Select(u.typeModel)
types, err := m.Get(param)
if err != nil {
return nil, fmt.Errorf(ErrFailedToGetType, err)
}
return types, nil
}
// PaginateTypes retrieves paginated list of types
func (u *DefaultUser) PaginateTypes(ctx context.Context, param model.QueryParam, page int, pagesize int) (maps.MapStr, error) {
// TODO: implement
return nil, nil
// Set default select fields if not provided
if param.Select == nil {
param.Select = u.typeFields
}
m := model.Select(u.typeModel)
result, err := m.Paginate(param, page, pagesize)
if err != nil {
return nil, fmt.Errorf(ErrFailedToGetType, err)
}
return result, nil
}
// CountTypes returns total count of types with optional filters
func (u *DefaultUser) CountTypes(ctx context.Context, param model.QueryParam) (int64, error) {
// TODO: implement
return 0, nil
// Use Paginate with a small page size to get the total count
// This is more reliable than manual COUNT(*) queries
m := model.Select(u.typeModel)
result, err := m.Paginate(param, 1, 1) // Get first page with 1 item to get total
if err != nil {
return 0, fmt.Errorf(ErrFailedToGetType, err)
}
// Extract total from pagination result
if total, ok := result["total"].(int64); ok {
return total, nil
}
// Handle different total types returned by Paginate
if totalInterface, ok := result["total"]; ok {
switch v := totalInterface.(type) {
case int:
return int64(v), nil
case int32:
return int64(v), nil
case int64:
return v, nil
case uint:
return int64(v), nil
case uint32:
return int64(v), nil
case uint64:
return int64(v), nil
default:
return 0, fmt.Errorf("unexpected total type: %T", totalInterface)
}
}
return 0, fmt.Errorf("total not found in pagination result")
}
// GetTypeConfiguration retrieves configuration for a type (schema, features, limits, etc.)
func (u *DefaultUser) GetTypeConfiguration(ctx context.Context, typeID string) (maps.MapStrAny, error) {
// TODO: implement
return nil, nil
m := model.Select(u.typeModel)
types, err := m.Get(model.QueryParam{
Select: []interface{}{"type_id", "schema", "features", "limits", "password_policy", "metadata"},
Wheres: []model.QueryWhere{
{Column: "type_id", Value: typeID},
},
Limit: 1,
})
if err != nil {
return nil, fmt.Errorf(ErrFailedToGetType, err)
}
if len(types) == 0 {
return nil, fmt.Errorf(ErrTypeNotFound)
}
typeRecord := types[0]
config := maps.MapStrAny{
"type_id": typeID,
"schema": typeRecord["schema"],
"features": typeRecord["features"],
"limits": typeRecord["limits"],
"password_policy": typeRecord["password_policy"],
"metadata": typeRecord["metadata"],
}
return config, nil
}
// SetTypeConfiguration sets configuration for a type
func (u *DefaultUser) SetTypeConfiguration(ctx context.Context, typeID string, config maps.MapStrAny) error {
// TODO: implement
// Prepare update data - only allow configuration-related fields
updateData := maps.MapStrAny{}
if schema, ok := config["schema"]; ok {
updateData["schema"] = schema
}
if features, ok := config["features"]; ok {
updateData["features"] = features
}
if limits, ok := config["limits"]; ok {
updateData["limits"] = limits
}
if passwordPolicy, ok := config["password_policy"]; ok {
updateData["password_policy"] = passwordPolicy
}
if metadata, ok := config["metadata"]; ok {
updateData["metadata"] = metadata
}
// Skip update if no configuration fields provided
if len(updateData) == 0 {
return nil
}
m := model.Select(u.typeModel)
affected, err := m.UpdateWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "type_id", Value: typeID},
},
Limit: 1, // Safety: ensure only one record is updated
}, updateData)
if err != nil {
return fmt.Errorf(ErrFailedToUpdateType, err)
}
if affected == 0 {
return fmt.Errorf(ErrTypeNotFound)
}
return nil
}

View file

@ -0,0 +1,812 @@
package user_test
import (
"context"
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/gou/model"
"github.com/yaoapp/kun/maps"
)
// TestTypeData represents test type data structure
type TestTypeData struct {
TypeID string `json:"type_id"`
Name string `json:"name"`
Description string `json:"description"`
IsActive bool `json:"is_active"`
IsDefault bool `json:"is_default"`
SortOrder int `json:"sort_order"`
DefaultRoleID string `json:"default_role_id"`
MaxSessions *int `json:"max_sessions"`
SessionTimeout int `json:"session_timeout"`
Schema map[string]interface{} `json:"schema"`
Features map[string]interface{} `json:"features"`
Limits map[string]interface{} `json:"limits"`
PasswordPolicy map[string]interface{} `json:"password_policy"`
Metadata map[string]interface{} `json:"metadata"`
}
func TestTypeBasicOperations(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 type data dynamically
maxSessions := 5
testType := &TestTypeData{
TypeID: "testtype_" + testUUID,
Name: "Test Type " + testUUID,
Description: "Test type for unit testing " + testUUID,
IsActive: true,
IsDefault: false,
SortOrder: 100,
DefaultRoleID: "user",
MaxSessions: &maxSessions,
SessionTimeout: 3600,
Schema: map[string]interface{}{
"version": "1.0",
"fields": map[string]interface{}{
"profile": map[string]interface{}{
"required": true,
"type": "object",
},
},
},
Features: map[string]interface{}{
"mfa_enabled": true,
"api_access": true,
"export_data": false,
"custom_branding": true,
},
Limits: map[string]interface{}{
"storage_mb": 1024,
"api_calls_day": 10000,
"team_members": 50,
"projects": 10,
},
PasswordPolicy: map[string]interface{}{
"min_length": 8,
"require_uppercase": true,
"require_lowercase": true,
"require_numbers": true,
"require_symbols": false,
"max_age_days": 90,
},
Metadata: map[string]interface{}{
"source": "test",
"uuid": testUUID,
"version": "1.0",
},
}
// Test CreateType
t.Run("CreateType", func(t *testing.T) {
typeData := maps.MapStrAny{
"type_id": testType.TypeID,
"name": testType.Name,
"description": testType.Description,
"sort_order": testType.SortOrder,
"default_role_id": testType.DefaultRoleID,
"max_sessions": testType.MaxSessions,
"session_timeout": testType.SessionTimeout,
"schema": testType.Schema,
"features": testType.Features,
"limits": testType.Limits,
"password_policy": testType.PasswordPolicy,
"metadata": testType.Metadata,
}
id, err := testProvider.CreateType(ctx, typeData)
assert.NoError(t, err)
assert.NotNil(t, id)
// Verify default values were set
assert.Equal(t, true, typeData["is_active"])
assert.Equal(t, false, typeData["is_default"])
// sort_order, max_sessions, session_timeout should remain as provided
})
// Test GetType
t.Run("GetType", func(t *testing.T) {
typeRecord, err := testProvider.GetType(ctx, testType.TypeID)
assert.NoError(t, err)
assert.NotNil(t, typeRecord)
// Verify key fields
assert.Equal(t, testType.TypeID, typeRecord["type_id"])
assert.Equal(t, testType.Name, typeRecord["name"])
assert.Equal(t, testType.Description, typeRecord["description"])
assert.Equal(t, testType.DefaultRoleID, typeRecord["default_role_id"])
// Handle different boolean representations from database
isActive := typeRecord["is_active"]
switch v := isActive.(type) {
case bool:
assert.True(t, v)
case int, int32, int64:
assert.NotEqual(t, 0, v) // Any non-zero value is true
default:
t.Errorf("unexpected is_active type: %T, value: %v", isActive, isActive)
}
assert.NotNil(t, typeRecord["created_at"])
})
// Test UpdateType
t.Run("UpdateType", func(t *testing.T) {
newMaxSessions := 10
updateData := maps.MapStrAny{
"name": "Updated Test Type",
"description": "Updated description for testing",
"sort_order": 200,
"default_role_id": "admin",
"max_sessions": &newMaxSessions,
"session_timeout": 7200,
"metadata": map[string]interface{}{
"updated": true,
"version": "2.0",
},
}
err := testProvider.UpdateType(ctx, testType.TypeID, updateData)
assert.NoError(t, err)
// Verify update
typeRecord, err := testProvider.GetType(ctx, testType.TypeID)
assert.NoError(t, err)
assert.Equal(t, "Updated Test Type", typeRecord["name"])
assert.Equal(t, "Updated description for testing", typeRecord["description"])
assert.Equal(t, "admin", typeRecord["default_role_id"])
// Test updating sensitive fields (should be ignored)
sensitiveData := maps.MapStrAny{
"id": 999,
"type_id": "malicious_type_id",
"created_at": "2020-01-01T00:00:00Z",
}
err = testProvider.UpdateType(ctx, testType.TypeID, sensitiveData)
assert.NoError(t, err) // Should not error, just ignore sensitive fields
// Verify sensitive fields were not changed
typeRecord, err = testProvider.GetType(ctx, testType.TypeID)
assert.NoError(t, err)
assert.Equal(t, testType.TypeID, typeRecord["type_id"]) // Should remain unchanged
})
// Test DeleteType (at the end)
t.Run("DeleteType", func(t *testing.T) {
err := testProvider.DeleteType(ctx, testType.TypeID)
assert.NoError(t, err)
// Verify type was deleted
_, err = testProvider.GetType(ctx, testType.TypeID)
assert.Error(t, err)
assert.Contains(t, err.Error(), "type not found")
})
}
func TestTypeConfigurationOperations(t *testing.T) {
prepare(t)
defer clean()
ctx := context.Background()
// Use UUID to ensure unique identifiers
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
// Create a type for configuration testing
testType := &TestTypeData{
TypeID: "configtype_" + testUUID,
Name: "Config Test Type " + testUUID,
Description: "Type for testing configuration",
IsActive: true,
Schema: map[string]interface{}{
"version": "1.0",
"type": "premium",
},
Features: map[string]interface{}{
"api_access": true,
"advanced_reports": true,
"custom_integrations": false,
"scope_limits": []interface{}{
"read", "write", "admin.read",
},
},
Limits: map[string]interface{}{
"storage_gb": 10,
"users": 100,
"api_calls": 50000,
},
PasswordPolicy: map[string]interface{}{
"min_length": 12,
"require_symbols": true,
"history_count": 5,
},
Metadata: map[string]interface{}{
"plan": "premium",
"tier": 2,
"features": "advanced",
},
}
// Create type
typeData := maps.MapStrAny{
"type_id": testType.TypeID,
"name": testType.Name,
"description": testType.Description,
"schema": testType.Schema,
"features": testType.Features,
"limits": testType.Limits,
"password_policy": testType.PasswordPolicy,
"metadata": testType.Metadata,
}
_, err := testProvider.CreateType(ctx, typeData)
assert.NoError(t, err)
// Test GetTypeConfiguration
t.Run("GetTypeConfiguration", func(t *testing.T) {
config, err := testProvider.GetTypeConfiguration(ctx, testType.TypeID)
assert.NoError(t, err)
assert.NotNil(t, config)
assert.Equal(t, testType.TypeID, config["type_id"])
assert.NotNil(t, config["schema"])
assert.NotNil(t, config["features"])
assert.NotNil(t, config["limits"])
assert.NotNil(t, config["password_policy"])
assert.NotNil(t, config["metadata"])
// Verify schema structure
schemaMap, ok := config["schema"].(map[string]interface{})
if ok {
assert.Equal(t, "1.0", schemaMap["version"])
assert.Equal(t, "premium", schemaMap["type"])
}
// Verify features structure
featuresMap, ok := config["features"].(map[string]interface{})
if ok {
assert.Equal(t, true, featuresMap["api_access"])
assert.Equal(t, true, featuresMap["advanced_reports"])
assert.Equal(t, false, featuresMap["custom_integrations"])
}
})
// Test SetTypeConfiguration
t.Run("SetTypeConfiguration", func(t *testing.T) {
newConfig := maps.MapStrAny{
"schema": map[string]interface{}{
"version": "2.0",
"type": "enterprise", // Changed
},
"features": map[string]interface{}{
"api_access": true,
"advanced_reports": true,
"custom_integrations": true, // Changed
"white_label": true, // New
"scope_limits": []interface{}{
"read", "write", "admin.read", "admin.write", // Extended
},
},
"limits": map[string]interface{}{
"storage_gb": 50, // Increased
"users": 500, // Increased
"api_calls": 100000, // Increased
},
"password_policy": map[string]interface{}{
"min_length": 16, // Increased
"require_symbols": true,
"history_count": 10, // Increased
"complexity_score": 8, // New
},
"metadata": map[string]interface{}{
"plan": "enterprise", // Changed
"tier": 3, // Changed
"features": "premium",
"updated_by": "test", // New
},
}
err := testProvider.SetTypeConfiguration(ctx, testType.TypeID, newConfig)
assert.NoError(t, err)
// Verify configuration was updated
config, err := testProvider.GetTypeConfiguration(ctx, testType.TypeID)
assert.NoError(t, err)
// Verify schema update
schemaMap, ok := config["schema"].(map[string]interface{})
if ok {
assert.Equal(t, "2.0", schemaMap["version"])
assert.Equal(t, "enterprise", schemaMap["type"]) // Should be updated
}
// Verify features update
featuresMap, ok := config["features"].(map[string]interface{})
if ok {
assert.Equal(t, true, featuresMap["custom_integrations"]) // Should be updated
assert.Equal(t, true, featuresMap["white_label"]) // Should be new
}
// Verify limits update
limitsMap, ok := config["limits"].(map[string]interface{})
if ok {
// Handle different numeric types from database
storageInterface := limitsMap["storage_gb"]
switch v := storageInterface.(type) {
case int:
assert.Equal(t, 50, v)
case int32:
assert.Equal(t, int32(50), v)
case int64:
assert.Equal(t, int64(50), v)
case float64:
assert.Equal(t, float64(50), v)
default:
t.Errorf("unexpected storage_gb type: %T, value: %v", storageInterface, storageInterface)
}
}
})
// Test SetTypeConfiguration with partial data
t.Run("SetTypeConfiguration_PartialUpdate", func(t *testing.T) {
partialConfig := maps.MapStrAny{
"metadata": map[string]interface{}{
"plan": "enterprise",
"tier": 3,
"features": "premium",
"updated": true, // New field
"timestamp": "2024-01-01", // New field
},
}
err := testProvider.SetTypeConfiguration(ctx, testType.TypeID, partialConfig)
assert.NoError(t, err)
// Verify only metadata was updated, other configs remain
config, err := testProvider.GetTypeConfiguration(ctx, testType.TypeID)
assert.NoError(t, err)
// Schema should remain from previous update
schemaMap, ok := config["schema"].(map[string]interface{})
if ok {
assert.Equal(t, "2.0", schemaMap["version"])
}
// Metadata should be updated
metadataMap, ok := config["metadata"].(map[string]interface{})
if ok {
assert.Equal(t, true, metadataMap["updated"])
assert.Equal(t, "2024-01-01", metadataMap["timestamp"])
}
})
// Test SetTypeConfiguration with empty data (should not error)
t.Run("SetTypeConfiguration_EmptyData", func(t *testing.T) {
emptyConfig := maps.MapStrAny{}
err := testProvider.SetTypeConfiguration(ctx, testType.TypeID, emptyConfig)
assert.NoError(t, err) // Should not error, just skip update
})
}
func TestTypeListOperations(t *testing.T) {
prepare(t)
defer clean()
ctx := context.Background()
// Create multiple test types for list operations
// Use UUID to ensure unique identifiers
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
testTypes := []TestTypeData{
{
TypeID: "listtype_" + testUUID + "_1",
Name: "List Type 1",
Description: "First type for list testing",
IsActive: true,
SortOrder: 10,
},
{
TypeID: "listtype_" + testUUID + "_2",
Name: "List Type 2",
Description: "Second type for list testing",
IsActive: true,
SortOrder: 20,
},
{
TypeID: "listtype_" + testUUID + "_3",
Name: "List Type 3",
Description: "Third type for list testing",
IsActive: false, // Different status for filtering
SortOrder: 30,
},
{
TypeID: "listtype_" + testUUID + "_4",
Name: "List Type 4",
Description: "Fourth type for list testing",
IsActive: true,
SortOrder: 40,
},
{
TypeID: "listtype_" + testUUID + "_5",
Name: "List Type 5",
Description: "Fifth type for list testing",
IsActive: true,
SortOrder: 50,
},
}
// Create types in database
for _, typeData := range testTypes {
typeMap := maps.MapStrAny{
"type_id": typeData.TypeID,
"name": typeData.Name,
"description": typeData.Description,
"is_active": typeData.IsActive,
"sort_order": typeData.SortOrder,
}
_, err := testProvider.CreateType(ctx, typeMap)
assert.NoError(t, err)
}
// Test GetTypes
t.Run("GetTypes_All", func(t *testing.T) {
param := model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "type_id", OP: "like", Value: "listtype_" + testUUID + "_%"},
},
}
types, err := testProvider.GetTypes(ctx, param)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(types), 5) // At least our 5 test types
// Check that basic fields are returned by default
if len(types) > 0 {
typeRecord := types[0]
assert.Contains(t, typeRecord, "type_id")
assert.Contains(t, typeRecord, "name")
assert.Contains(t, typeRecord, "description")
assert.Contains(t, typeRecord, "is_active")
}
})
t.Run("GetTypes_WithFilters", func(t *testing.T) {
param := model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "type_id", OP: "like", Value: "listtype_" + testUUID + "_%"},
{Column: "is_active", Value: true},
},
}
types, err := testProvider.GetTypes(ctx, param)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(types), 4) // At least 4 active types
// All returned types should be active
for _, typeRecord := range types {
if strings.Contains(typeRecord["type_id"].(string), "listtype_"+testUUID+"_") {
// Handle different boolean representations from database
isActive := typeRecord["is_active"]
switch v := isActive.(type) {
case bool:
assert.True(t, v)
case int, int32, int64:
assert.NotEqual(t, 0, v) // Any non-zero value is true
default:
t.Errorf("unexpected is_active type: %T, value: %v", isActive, isActive)
}
}
}
})
t.Run("GetTypes_WithCustomFields", func(t *testing.T) {
param := model.QueryParam{
Select: []interface{}{"type_id", "name", "is_active", "sort_order"},
Wheres: []model.QueryWhere{
{Column: "type_id", OP: "like", Value: "listtype_" + testUUID + "_%"},
},
Limit: 3,
}
types, err := testProvider.GetTypes(ctx, param)
assert.NoError(t, err)
assert.LessOrEqual(t, len(types), 3) // Respects limit
if len(types) > 0 {
typeRecord := types[0]
assert.Contains(t, typeRecord, "type_id")
assert.Contains(t, typeRecord, "name")
assert.Contains(t, typeRecord, "is_active")
assert.Contains(t, typeRecord, "sort_order")
}
})
// Test PaginateTypes
t.Run("PaginateTypes_FirstPage", func(t *testing.T) {
param := model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "type_id", OP: "like", Value: "listtype_" + testUUID + "_%"},
},
Orders: []model.QueryOrder{
{Column: "sort_order", Option: "asc"},
},
}
result, err := testProvider.PaginateTypes(ctx, param, 1, 3)
assert.NoError(t, err)
assert.NotNil(t, result)
// Check pagination structure
assert.Contains(t, result, "data")
assert.Contains(t, result, "total")
assert.Contains(t, result, "page")
assert.Contains(t, result, "pagesize")
data, ok := result["data"].([]maps.MapStr)
assert.True(t, ok)
assert.LessOrEqual(t, len(data), 3) // Page size limit
// Handle different total types
totalInterface, exists := result["total"]
assert.True(t, exists)
var total int64
switch v := totalInterface.(type) {
case int:
total = int64(v)
case int32:
total = int64(v)
case int64:
total = v
case uint:
total = int64(v)
case uint32:
total = int64(v)
case uint64:
total = int64(v)
default:
t.Errorf("unexpected total type: %T, value: %v", totalInterface, totalInterface)
}
assert.GreaterOrEqual(t, total, int64(5)) // At least 5 types
assert.Equal(t, 1, result["page"])
assert.Equal(t, 3, result["pagesize"])
})
t.Run("PaginateTypes_WithFilters", func(t *testing.T) {
param := model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "type_id", OP: "like", Value: "listtype_" + testUUID + "_%"},
{Column: "is_active", Value: true},
},
}
result, err := testProvider.PaginateTypes(ctx, param, 1, 10)
assert.NoError(t, err)
assert.NotNil(t, result)
data, ok := result["data"].([]maps.MapStr)
assert.True(t, ok)
assert.GreaterOrEqual(t, len(data), 4) // At least 4 active types
// Verify is_active filter works
for _, typeRecord := range data {
if strings.Contains(typeRecord["type_id"].(string), "listtype_"+testUUID+"_") {
// Handle different boolean representations from database
isActive := typeRecord["is_active"]
switch v := isActive.(type) {
case bool:
assert.True(t, v)
case int, int32, int64:
assert.NotEqual(t, 0, v) // Any non-zero value is true
default:
t.Errorf("unexpected is_active type: %T, value: %v", isActive, isActive)
}
}
}
})
// Test CountTypes
t.Run("CountTypes_All", func(t *testing.T) {
param := model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "type_id", OP: "like", Value: "listtype_" + testUUID + "_%"},
},
}
count, err := testProvider.CountTypes(ctx, param)
assert.NoError(t, err)
assert.GreaterOrEqual(t, count, int64(5)) // At least 5 types
})
t.Run("CountTypes_WithFilters", func(t *testing.T) {
param := model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "type_id", OP: "like", Value: "listtype_" + testUUID + "_%"},
{Column: "is_active", Value: true},
},
}
count, err := testProvider.CountTypes(ctx, param)
assert.NoError(t, err)
assert.GreaterOrEqual(t, count, int64(4)) // At least 4 active types
})
t.Run("CountTypes_SpecificSortOrder", func(t *testing.T) {
param := model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "type_id", OP: "like", Value: "listtype_" + testUUID + "_%"},
{Column: "sort_order", OP: ">=", Value: 30},
},
}
count, err := testProvider.CountTypes(ctx, param)
assert.NoError(t, err)
// We created 3 types with sort_order >= 30 (30, 40, 50), but be flexible with database state
assert.GreaterOrEqual(t, count, int64(1)) // At least 1 type with sort_order >= 30
assert.LessOrEqual(t, count, int64(5)) // But not more than 5 (our total test types)
})
t.Run("CountTypes_NoResults", func(t *testing.T) {
param := model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "type_id", Value: "nonexistent_type_id"},
},
}
count, err := testProvider.CountTypes(ctx, param)
assert.NoError(t, err)
assert.Equal(t, int64(0), count)
})
}
func TestTypeErrorHandling(t *testing.T) {
prepare(t)
defer clean()
ctx := context.Background()
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
nonExistentTypeID := "nonexistent_type_" + testUUID
t.Run("GetType_NotFound", func(t *testing.T) {
_, err := testProvider.GetType(ctx, nonExistentTypeID)
assert.Error(t, err)
assert.Contains(t, err.Error(), "type not found")
})
t.Run("CreateType_MissingTypeID", func(t *testing.T) {
typeData := maps.MapStrAny{
"name": "Test Type",
"description": "Type without type_id",
}
_, err := testProvider.CreateType(ctx, typeData)
assert.Error(t, err)
assert.Contains(t, err.Error(), "type_id is required")
})
t.Run("UpdateType_NotFound", func(t *testing.T) {
updateData := maps.MapStrAny{"name": "Test"}
err := testProvider.UpdateType(ctx, nonExistentTypeID, updateData)
assert.Error(t, err)
assert.Contains(t, err.Error(), "type not found")
})
t.Run("DeleteType_NotFound", func(t *testing.T) {
err := testProvider.DeleteType(ctx, nonExistentTypeID)
assert.Error(t, err)
assert.Contains(t, err.Error(), "type not found")
})
t.Run("GetTypeConfiguration_NotFound", func(t *testing.T) {
_, err := testProvider.GetTypeConfiguration(ctx, nonExistentTypeID)
assert.Error(t, err)
assert.Contains(t, err.Error(), "type not found")
})
t.Run("SetTypeConfiguration_NotFound", func(t *testing.T) {
config := maps.MapStrAny{
"schema": map[string]interface{}{"test": true},
}
err := testProvider.SetTypeConfiguration(ctx, nonExistentTypeID, config)
assert.Error(t, err)
assert.Contains(t, err.Error(), "type not found")
})
t.Run("GetTypes_EmptyResult", func(t *testing.T) {
param := model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "type_id", Value: nonExistentTypeID},
},
}
types, err := testProvider.GetTypes(ctx, param)
assert.NoError(t, err)
assert.Equal(t, 0, len(types)) // Empty slice, not nil
})
t.Run("PaginateTypes_EmptyResult", func(t *testing.T) {
param := model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "type_id", Value: nonExistentTypeID},
},
}
result, err := testProvider.PaginateTypes(ctx, param, 1, 10)
assert.NoError(t, err)
assert.NotNil(t, result)
data, ok := result["data"].([]maps.MapStr)
assert.True(t, ok)
assert.Equal(t, 0, len(data))
// Handle different total types
totalInterface, exists := result["total"]
assert.True(t, exists)
var total int64
switch v := totalInterface.(type) {
case int:
total = int64(v)
case int32:
total = int64(v)
case int64:
total = v
case uint:
total = int64(v)
case uint32:
total = int64(v)
case uint64:
total = int64(v)
default:
t.Errorf("unexpected total type: %T, value: %v", totalInterface, totalInterface)
}
assert.Equal(t, int64(0), total)
})
t.Run("UpdateType_EmptyData", func(t *testing.T) {
// First create a type for this test
testTypeID := "emptyupdate_" + testUUID
typeData := maps.MapStrAny{
"type_id": testTypeID,
"name": "Test Type for Empty Update",
}
_, err := testProvider.CreateType(ctx, typeData)
assert.NoError(t, err)
// Test with empty update data (should not error, just do nothing)
emptyData := maps.MapStrAny{}
err = testProvider.UpdateType(ctx, testTypeID, emptyData)
assert.NoError(t, err) // Should not error, just skip update
})
t.Run("SetTypeConfiguration_EmptyData", func(t *testing.T) {
// First create a type for this test
testTypeID := "emptyconfig_" + testUUID
typeData := maps.MapStrAny{
"type_id": testTypeID,
"name": "Test Type for Empty Configuration",
}
_, err := testProvider.CreateType(ctx, typeData)
assert.NoError(t, err)
// Test with empty configuration data (should not error, just do nothing)
emptyData := maps.MapStrAny{}
err = testProvider.SetTypeConfiguration(ctx, testTypeID, emptyData)
assert.NoError(t, err) // Should not error, just skip update
})
t.Run("CountTypes_ComplexFilters", func(t *testing.T) {
param := model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "is_active", Value: true},
{Column: "sort_order", OP: ">=", Value: 10},
{Column: "is_default", Value: false},
},
}
count, err := testProvider.CountTypes(ctx, param)
assert.NoError(t, err)
assert.GreaterOrEqual(t, count, int64(0)) // Should handle complex filters without error
})
}

View file

@ -6,6 +6,7 @@ import (
"time"
"github.com/yaoapp/gou/model"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/kun/maps"
"golang.org/x/crypto/bcrypt"
)
@ -255,9 +256,48 @@ func (u *DefaultUser) UpdateUser(ctx context.Context, userID string, userData ma
return nil
}
// DeleteUser soft deletes a user account
// DeleteUser soft deletes a user account and all associated data
func (u *DefaultUser) DeleteUser(ctx context.Context, userID string) error {
// First verify the user exists
m := model.Select(u.model)
users, err := m.Get(model.QueryParam{
Select: []interface{}{"user_id"},
Wheres: []model.QueryWhere{
{Column: "user_id", Value: userID},
},
Limit: 1,
})
if err != nil {
return fmt.Errorf(ErrFailedToGetUser, err)
}
if len(users) == 0 {
return fmt.Errorf(ErrUserNotFound)
}
// Clean up associated data before deleting the user
// Note: We log warnings for cleanup failures but don't fail the user deletion
// 1. Delete all OAuth accounts for this user
err = u.DeleteUserOAuthAccounts(ctx, userID)
if err != nil {
log.Warn("Failed to delete OAuth accounts for user %s: %v", userID, err)
}
// 2. Clear user role assignment (set role_id to null)
err = u.ClearUserRole(ctx, userID)
if err != nil {
log.Warn("Failed to clear role assignment for user %s: %v", userID, err)
}
// 3. Clear user type assignment (set type_id to null)
err = u.ClearUserType(ctx, userID)
if err != nil {
log.Warn("Failed to clear type assignment for user %s: %v", userID, err)
}
// 4. Finally, delete the user account
affected, err := m.DeleteWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "user_id", Value: userID},

View file

@ -153,18 +153,219 @@ func (u *DefaultUser) ClearUserRole(ctx context.Context, userID string) error {
// GetUserType retrieves user's type information
func (u *DefaultUser) GetUserType(ctx context.Context, userID string) (maps.MapStrAny, error) {
// TODO: implement
return nil, nil
// First get the user's type_id
userModel := model.Select(u.model)
users, err := userModel.Get(model.QueryParam{
Select: []interface{}{"user_id", "type_id"},
Wheres: []model.QueryWhere{
{Column: "user_id", Value: userID},
},
Limit: 1,
})
if err != nil {
return nil, fmt.Errorf(ErrFailedToGetUser, err)
}
if len(users) == 0 {
return nil, fmt.Errorf(ErrUserNotFound)
}
user := users[0]
typeID, ok := user["type_id"].(string)
if !ok || typeID == "" {
return nil, fmt.Errorf("user %s has no type assigned", userID)
}
// Now get the full type information
typeModel := model.Select(u.typeModel)
types, err := typeModel.Get(model.QueryParam{
Select: u.typeFields,
Wheres: []model.QueryWhere{
{Column: "type_id", Value: typeID},
},
Limit: 1,
})
if err != nil {
return nil, fmt.Errorf(ErrFailedToGetType, err)
}
if len(types) == 0 {
return nil, fmt.Errorf(ErrTypeNotFound)
}
return types[0], nil
}
// SetUserType assigns a type to a user
func (u *DefaultUser) SetUserType(ctx context.Context, userID string, typeID string) error {
// TODO: implement
// First validate that the type exists
typeModel := model.Select(u.typeModel)
types, err := typeModel.Get(model.QueryParam{
Select: []interface{}{"type_id", "is_active"},
Wheres: []model.QueryWhere{
{Column: "type_id", Value: typeID},
},
Limit: 1,
})
if err != nil {
return fmt.Errorf(ErrFailedToGetType, err)
}
if len(types) == 0 {
return fmt.Errorf(ErrTypeNotFound)
}
// Check if type is active
typeRecord := types[0]
if isActive, ok := typeRecord["is_active"].(bool); ok && !isActive {
return fmt.Errorf("cannot assign inactive type: %s", typeID)
}
// Handle different boolean types from database
if isActiveInt, ok := typeRecord["is_active"].(int64); ok && isActiveInt == 0 {
return fmt.Errorf("cannot assign inactive type: %s", typeID)
}
// Update user's type_id
updateData := maps.MapStrAny{
"type_id": typeID,
}
userModel := model.Select(u.model)
affected, err := userModel.UpdateWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "user_id", Value: userID},
},
Limit: 1, // Safety: ensure only one record is updated
}, updateData)
if err != nil {
return fmt.Errorf(ErrFailedToUpdateUser, err)
}
if affected == 0 {
return fmt.Errorf(ErrUserNotFound)
}
return nil
}
// ClearUserType removes type assignment from a user (sets type_id to null)
func (u *DefaultUser) ClearUserType(ctx context.Context, userID string) error {
// First check if user exists
userModel := model.Select(u.model)
users, err := userModel.Get(model.QueryParam{
Select: []interface{}{"user_id"},
Wheres: []model.QueryWhere{
{Column: "user_id", Value: userID},
},
Limit: 1,
})
if err != nil {
return fmt.Errorf(ErrFailedToGetUser, err)
}
if len(users) == 0 {
return fmt.Errorf(ErrUserNotFound)
}
// Update type_id to null (even if it's already null, this should succeed)
updateData := maps.MapStrAny{
"type_id": nil, // Set type_id to null to clear type assignment
}
_, err = userModel.UpdateWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "user_id", Value: userID},
},
Limit: 1, // Safety: ensure only one record is updated
}, updateData)
if err != nil {
return fmt.Errorf(ErrFailedToUpdateUser, err)
}
// Don't check affected rows - setting null to null is still a successful operation
return nil
}
// ValidateUserScope validates if a user has access to requested scopes based on role and type
func (u *DefaultUser) ValidateUserScope(ctx context.Context, userID string, scopes []string) (bool, error) {
// TODO: implement
return false, nil
if len(scopes) == 0 {
return true, nil // No scopes required
}
// Get user's role
userRole, err := u.GetUserRole(ctx, userID)
if err != nil {
// If user has no role, check if scopes are required
if err.Error() == fmt.Sprintf("user %s has no role assigned", userID) {
// Users without roles have minimal access (empty scopes only)
return len(scopes) == 0, nil
}
return false, err
}
// Extract role_id for permission validation
roleID, ok := userRole["role_id"].(string)
if !ok {
return false, fmt.Errorf("invalid role_id format")
}
// Use role-based permission validation
valid, err := u.ValidateRolePermissions(ctx, roleID, scopes)
if err != nil {
return false, err
}
// If role validation passes, check type-specific restrictions if applicable
if valid {
// Get user's type for additional validation
userType, err := u.GetUserType(ctx, userID)
if err != nil {
// If user has no type, role validation is sufficient
if err.Error() == fmt.Sprintf("user %s has no type assigned", userID) {
return valid, nil
}
return false, err
}
// Get type configuration to check for additional scope restrictions
typeID, ok := userType["type_id"].(string)
if !ok {
return false, fmt.Errorf("invalid type_id format")
}
typeConfig, err := u.GetTypeConfiguration(ctx, typeID)
if err != nil {
return false, err
}
// Check if type has specific scope limitations
if features, ok := typeConfig["features"].(map[string]interface{}); ok {
if scopeLimits, exists := features["scope_limits"]; exists {
if limitList, ok := scopeLimits.([]interface{}); ok {
// If type has scope limits, ensure all requested scopes are allowed
allowedScopes := make(map[string]bool)
for _, scope := range limitList {
if scopeStr, ok := scope.(string); ok {
allowedScopes[scopeStr] = true
}
}
// Check each requested scope against type limits
for _, scope := range scopes {
if !allowedScopes[scope] {
return false, nil // Scope not allowed by type
}
}
}
}
}
}
return valid, nil
}

View file

@ -7,6 +7,7 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/gou/model"
"github.com/yaoapp/kun/maps"
)
@ -157,23 +158,127 @@ func TestUserTypeOperations(t *testing.T) {
testUser := createTestUserData("typeuser" + testUUID)
_, testUserID := setupTestUser(t, ctx, testUser)
// Note: User type operations are not yet implemented
// These tests are placeholders for future implementation
// Step 2: Create test types for assignment
testTypes := []maps.MapStrAny{
{
"type_id": "basictype_" + testUUID,
"name": "Basic Type " + testUUID,
"description": "Basic user type for testing",
"is_active": true,
"sort_order": 10,
},
{
"type_id": "premiumtype_" + testUUID,
"name": "Premium Type " + testUUID,
"description": "Premium user type for testing",
"is_active": true,
"sort_order": 20,
},
{
"type_id": "inactivetype_" + testUUID,
"name": "Inactive Type " + testUUID,
"description": "Inactive type for testing",
"is_active": false,
"sort_order": 0,
},
}
// Test GetUserType (should return not implemented or similar)
t.Run("GetUserType_NotImplemented", func(t *testing.T) {
_, err := testProvider.GetUserType(ctx, testUserID)
// Since implementation returns nil, nil - we expect no error but nil result
// In a real implementation, this might return an error or the actual type
assert.NoError(t, err) // Based on current TODO implementation
// Create types in database
for _, typeData := range testTypes {
_, err := testProvider.CreateType(ctx, typeData)
assert.NoError(t, err)
}
basicTypeID := "basictype_" + testUUID
premiumTypeID := "premiumtype_" + testUUID
inactiveTypeID := "inactivetype_" + testUUID
// Test SetUserType
t.Run("SetUserType", func(t *testing.T) {
err := testProvider.SetUserType(ctx, testUserID, basicTypeID)
assert.NoError(t, err)
// Verify type was assigned by getting user info
user, err := testProvider.GetUser(ctx, testUserID)
assert.NoError(t, err)
assert.Equal(t, basicTypeID, user["type_id"])
})
// Test SetUserType (should return not implemented or similar)
t.Run("SetUserType_NotImplemented", func(t *testing.T) {
err := testProvider.SetUserType(ctx, testUserID, "premium")
// Since implementation returns nil - we expect no error
// In a real implementation, this might return an error or actually set the type
assert.NoError(t, err) // Based on current TODO implementation
// Test GetUserType
t.Run("GetUserType", func(t *testing.T) {
userType, err := testProvider.GetUserType(ctx, testUserID)
assert.NoError(t, err)
assert.NotNil(t, userType)
// Verify we got the correct type information
assert.Equal(t, basicTypeID, userType["type_id"])
assert.Equal(t, "Basic Type "+testUUID, userType["name"])
assert.Equal(t, "Basic user type for testing", userType["description"])
// Handle different boolean representations from database
isActive := userType["is_active"]
switch v := isActive.(type) {
case bool:
assert.True(t, v)
case int, int32, int64:
assert.NotEqual(t, 0, v) // Any non-zero value is true
default:
t.Errorf("unexpected is_active type: %T, value: %v", isActive, isActive)
}
})
// Test SetUserType - Change to different type
t.Run("SetUserType_ChangeType", func(t *testing.T) {
err := testProvider.SetUserType(ctx, testUserID, premiumTypeID)
assert.NoError(t, err)
// Verify type was changed
userType, err := testProvider.GetUserType(ctx, testUserID)
assert.NoError(t, err)
assert.Equal(t, premiumTypeID, userType["type_id"])
assert.Equal(t, "Premium Type "+testUUID, userType["name"])
})
// Test SetUserType - Inactive Type (should fail)
t.Run("SetUserType_InactiveType", func(t *testing.T) {
err := testProvider.SetUserType(ctx, testUserID, inactiveTypeID)
assert.Error(t, err)
assert.Contains(t, err.Error(), "cannot assign inactive type")
// Verify type was not changed
userType, err := testProvider.GetUserType(ctx, testUserID)
assert.NoError(t, err)
assert.Equal(t, premiumTypeID, userType["type_id"]) // Should still be the previous type
})
// Test ClearUserType
t.Run("ClearUserType", func(t *testing.T) {
err := testProvider.ClearUserType(ctx, testUserID)
assert.NoError(t, err)
// Verify type was cleared
_, err = testProvider.GetUserType(ctx, testUserID)
assert.Error(t, err)
assert.Contains(t, err.Error(), "has no type assigned")
// Verify user still exists
user, err := testProvider.GetUser(ctx, testUserID)
assert.NoError(t, err)
assert.Equal(t, testUserID, user["user_id"])
assert.Nil(t, user["type_id"]) // type_id should be null
})
// Test SetUserType - After Clear
t.Run("SetUserType_AfterClear", func(t *testing.T) {
// Re-assign a type after clearing
err := testProvider.SetUserType(ctx, testUserID, basicTypeID)
assert.NoError(t, err)
// Verify type was assigned
userType, err := testProvider.GetUserType(ctx, testUserID)
assert.NoError(t, err)
assert.Equal(t, basicTypeID, userType["type_id"])
assert.Equal(t, "Basic Type "+testUUID, userType["name"])
})
}
@ -186,21 +291,210 @@ func TestValidateUserScope(t *testing.T) {
// Use UUID to ensure unique identifiers
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
// Create a test user
// Step 1: Create test role with specific permissions
testRole := maps.MapStrAny{
"role_id": "scoperole_" + testUUID,
"name": "Scope Test Role " + testUUID,
"description": "Role for testing scope validation",
"is_active": true,
"permissions": map[string]interface{}{
"read": true,
"write": true,
"admin.read": true,
"admin.write": false,
"delete": false,
},
"restricted_permissions": []string{
"system.config",
"root.access",
},
}
_, err := testProvider.CreateRole(ctx, testRole)
assert.NoError(t, err)
// Step 2: Create test type with scope limitations
testType := maps.MapStrAny{
"type_id": "scopetype_" + testUUID,
"name": "Scope Test Type " + testUUID,
"description": "Type for testing scope validation",
"is_active": true,
"features": map[string]interface{}{
"api_access": true,
"scope_limits": []interface{}{
"read", "write", "admin.read", // Allowed scopes
},
},
}
_, err = testProvider.CreateType(ctx, testType)
assert.NoError(t, err)
// Step 3: Create test user and assign role and type
testUser := createTestUserData("scopeuser" + testUUID)
_, testUserID := setupTestUser(t, ctx, testUser)
// Note: ValidateUserScope is not yet implemented
// This test is a placeholder for future implementation
roleID := "scoperole_" + testUUID
typeID := "scopetype_" + testUUID
t.Run("ValidateUserScope_NotImplemented", func(t *testing.T) {
scopes := []string{"read", "write", "admin"}
valid, err := testProvider.ValidateUserScope(ctx, testUserID, scopes)
// Assign role and type to user
err = testProvider.SetUserRole(ctx, testUserID, roleID)
assert.NoError(t, err)
// Since implementation returns false, nil - we expect no error but false result
// In a real implementation, this would validate user's scopes based on role and type
assert.NoError(t, err) // Based on current TODO implementation
assert.False(t, valid) // Based on current TODO implementation
err = testProvider.SetUserType(ctx, testUserID, typeID)
assert.NoError(t, err)
// Test various scope validation scenarios
t.Run("ValidateUserScope_EmptyScopes", func(t *testing.T) {
valid, err := testProvider.ValidateUserScope(ctx, testUserID, []string{})
assert.NoError(t, err)
assert.True(t, valid) // Empty scopes should always be valid
})
t.Run("ValidateUserScope_ValidSingleScope", func(t *testing.T) {
valid, err := testProvider.ValidateUserScope(ctx, testUserID, []string{"read"})
assert.NoError(t, err)
assert.True(t, valid) // "read" is allowed by both role and type
})
t.Run("ValidateUserScope_ValidMultipleScopes", func(t *testing.T) {
valid, err := testProvider.ValidateUserScope(ctx, testUserID, []string{"read", "write"})
assert.NoError(t, err)
assert.True(t, valid) // Both "read" and "write" are allowed
})
t.Run("ValidateUserScope_ValidAdminReadScope", func(t *testing.T) {
valid, err := testProvider.ValidateUserScope(ctx, testUserID, []string{"admin.read"})
assert.NoError(t, err)
assert.True(t, valid) // "admin.read" is allowed by both role and type
})
t.Run("ValidateUserScope_InvalidRolePermission", func(t *testing.T) {
valid, err := testProvider.ValidateUserScope(ctx, testUserID, []string{"admin.write"})
assert.NoError(t, err)
assert.False(t, valid) // "admin.write" is denied by role permissions
})
t.Run("ValidateUserScope_RestrictedPermission", func(t *testing.T) {
valid, err := testProvider.ValidateUserScope(ctx, testUserID, []string{"system.config"})
assert.NoError(t, err)
assert.False(t, valid) // "system.config" is in restricted permissions
})
t.Run("ValidateUserScope_TypeScopeLimitation", func(t *testing.T) {
valid, err := testProvider.ValidateUserScope(ctx, testUserID, []string{"delete"})
assert.NoError(t, err)
assert.False(t, valid) // "delete" is not in type's scope_limits
})
t.Run("ValidateUserScope_MixedValidInvalid", func(t *testing.T) {
valid, err := testProvider.ValidateUserScope(ctx, testUserID, []string{"read", "delete"})
assert.NoError(t, err)
assert.False(t, valid) // Should fail because "delete" is not allowed
})
t.Run("ValidateUserScope_NonExistentScope", func(t *testing.T) {
valid, err := testProvider.ValidateUserScope(ctx, testUserID, []string{"nonexistent.permission"})
assert.NoError(t, err)
assert.False(t, valid) // Non-existent permissions should be denied
})
// Test user without role
t.Run("ValidateUserScope_UserWithoutRole", func(t *testing.T) {
// Create a user without role assignment
userWithoutRole := createTestUserData("noroleuser" + testUUID)
_, userWithoutRoleID := setupTestUser(t, ctx, userWithoutRole)
// Clear any default role that might have been set
err := testProvider.ClearUserRole(ctx, userWithoutRoleID)
assert.NoError(t, err)
// User without role should only have access to empty scopes
valid, err := testProvider.ValidateUserScope(ctx, userWithoutRoleID, []string{})
assert.NoError(t, err)
assert.True(t, valid) // Empty scopes should be valid
// Users without roles have minimal access (empty scopes only)
valid, err = testProvider.ValidateUserScope(ctx, userWithoutRoleID, []string{"read"})
assert.NoError(t, err)
assert.False(t, valid) // Should return false - users without roles can only access empty scopes
})
// Test user without type (type restrictions should not apply)
t.Run("ValidateUserScope_UserWithoutType", func(t *testing.T) {
// Create a user with role but without type
userWithoutType := createTestUserData("notypeuser" + testUUID)
userWithoutType.TypeID = "" // Explicitly clear type_id
_, userWithoutTypeID := setupTestUser(t, ctx, userWithoutType)
// Assign role but no type
err := testProvider.SetUserRole(ctx, userWithoutTypeID, roleID)
assert.NoError(t, err)
// Manually clear type_id to ensure user has no type
userModel := model.Select("__yao.user")
_, err = userModel.UpdateWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "user_id", Value: userWithoutTypeID},
},
Limit: 1,
}, maps.MapStrAny{
"type_id": nil, // Set type_id to null
})
assert.NoError(t, err)
// Verify user has no type assigned
_, err = testProvider.GetUserType(ctx, userWithoutTypeID)
assert.Error(t, err)
assert.Contains(t, err.Error(), "has no type assigned")
// Should be able to access permissions allowed by role (no type restrictions)
valid, err := testProvider.ValidateUserScope(ctx, userWithoutTypeID, []string{"read", "write"})
assert.NoError(t, err)
assert.True(t, valid) // Role allows these, no type restrictions
// Should still be restricted by role permissions
valid, err = testProvider.ValidateUserScope(ctx, userWithoutTypeID, []string{"admin.write"})
assert.NoError(t, err)
assert.False(t, valid) // Role denies this
})
// Test type without scope limits
t.Run("ValidateUserScope_TypeWithoutScopeLimits", func(t *testing.T) {
// Create a type without scope limits
openType := maps.MapStrAny{
"type_id": "opentype_" + testUUID,
"name": "Open Type " + testUUID,
"description": "Type without scope limitations",
"is_active": true,
"features": map[string]interface{}{
"api_access": true,
// No scope_limits - should allow anything the role permits
},
}
_, err := testProvider.CreateType(ctx, openType)
assert.NoError(t, err)
// Create user with role and open type
openUser := createTestUserData("openuser" + testUUID)
_, openUserID := setupTestUser(t, ctx, openUser)
err = testProvider.SetUserRole(ctx, openUserID, roleID)
assert.NoError(t, err)
err = testProvider.SetUserType(ctx, openUserID, "opentype_"+testUUID)
assert.NoError(t, err)
// Should be able to access any permission allowed by role
valid, err := testProvider.ValidateUserScope(ctx, openUserID, []string{"read", "write", "admin.read"})
assert.NoError(t, err)
assert.True(t, valid) // Type has no limitations, role allows these
// Should still be restricted by role permissions
valid, err = testProvider.ValidateUserScope(ctx, openUserID, []string{"admin.write"})
assert.NoError(t, err)
assert.False(t, valid) // Role denies this
})
}
@ -282,30 +576,96 @@ func TestUserRoleErrorHandling(t *testing.T) {
assert.NoError(t, err) // Should not error even if no role exists
})
// Test user type error handling (placeholders for future implementation)
// Create a valid type for some tests
validTypeData := maps.MapStrAny{
"type_id": "validtype_" + testUUID,
"name": "Valid Type " + testUUID,
"description": "Valid type for error testing",
"is_active": true,
}
_, err = testProvider.CreateType(ctx, validTypeData)
assert.NoError(t, err)
validTypeID := "validtype_" + testUUID
// Test user type error handling
t.Run("GetUserType_UserNotFound", func(t *testing.T) {
_, err := testProvider.GetUserType(ctx, nonExistentUserID)
// Since implementation returns nil, nil - we expect no error
// In a real implementation, this should return an error
assert.NoError(t, err) // Based on current TODO implementation
assert.Error(t, err)
assert.Contains(t, err.Error(), "user not found")
})
t.Run("GetUserType_NoTypeAssigned", func(t *testing.T) {
// Create a user without a type assignment
userWithoutType := createTestUserData("notypeuser" + testUUID)
userWithoutType.TypeID = "" // Explicitly clear type_id
_, userWithoutTypeID := setupTestUser(t, ctx, userWithoutType)
// Manually clear type_id to ensure user has no type
userModel := model.Select("__yao.user")
_, err = userModel.UpdateWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "user_id", Value: userWithoutTypeID},
},
Limit: 1,
}, maps.MapStrAny{
"type_id": nil, // Set type_id to null
})
assert.NoError(t, err)
_, err = testProvider.GetUserType(ctx, userWithoutTypeID)
assert.Error(t, err)
assert.Contains(t, err.Error(), "has no type assigned")
})
t.Run("SetUserType_UserNotFound", func(t *testing.T) {
err := testProvider.SetUserType(ctx, nonExistentUserID, "premium")
// Since implementation returns nil - we expect no error
// In a real implementation, this should return an error
assert.NoError(t, err) // Based on current TODO implementation
err := testProvider.SetUserType(ctx, nonExistentUserID, validTypeID)
assert.Error(t, err)
assert.Contains(t, err.Error(), "user not found")
})
// Test scope validation error handling (placeholder for future implementation)
t.Run("SetUserType_TypeNotFound", func(t *testing.T) {
nonExistentTypeID := "nonexistent_type_" + testUUID
err := testProvider.SetUserType(ctx, validUserID, nonExistentTypeID)
assert.Error(t, err)
assert.Contains(t, err.Error(), "type not found")
})
t.Run("ClearUserType_UserNotFound", func(t *testing.T) {
err := testProvider.ClearUserType(ctx, nonExistentUserID)
assert.Error(t, err)
assert.Contains(t, err.Error(), "user not found")
})
t.Run("ClearUserType_NoTypeTooClear", func(t *testing.T) {
// Create a user without type assignment
userWithoutType := createTestUserData("clearnotypeuser" + testUUID)
userWithoutType.TypeID = "" // Explicitly clear type_id
_, userWithoutTypeID := setupTestUser(t, ctx, userWithoutType)
// Manually clear type_id to ensure user has no type
userModel := model.Select("__yao.user")
_, err = userModel.UpdateWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "user_id", Value: userWithoutTypeID},
},
Limit: 1,
}, maps.MapStrAny{
"type_id": nil, // Set type_id to null
})
assert.NoError(t, err)
// Try to clear again (should still succeed)
err = testProvider.ClearUserType(ctx, userWithoutTypeID)
assert.NoError(t, err) // Should not error even if no type exists
})
// Test scope validation error handling
t.Run("ValidateUserScope_UserNotFound", func(t *testing.T) {
scopes := []string{"read", "write"}
valid, err := testProvider.ValidateUserScope(ctx, nonExistentUserID, scopes)
// Since implementation returns false, nil - we expect no error but false result
// In a real implementation, this should return an error
assert.NoError(t, err) // Based on current TODO implementation
assert.False(t, valid) // Based on current TODO implementation
assert.Error(t, err)
assert.False(t, valid)
assert.Contains(t, err.Error(), "user not found")
})
}

View file

@ -117,7 +117,7 @@ func cleanupTestData() {
rolePatterns := []string{
"test%", "%testrole%", "%listrole%", "%permrole%", "%adminrole%", "%userrole%",
"%inactiverole%", "%systemrole%", "%validrole%", "%emptyupdate%", "%emptyperm%",
"%guestrole%",
"%guestrole%", "%scoperole%",
}
for _, pattern := range rolePatterns {
roleModel.DestroyWhere(model.QueryParam{
@ -127,6 +127,21 @@ func cleanupTestData() {
})
}
// Clean types (should be done before users due to potential type_id references)
typeModel := model.Select("__yao.user_type")
typePatterns := []string{
"test%", "%testtype%", "%listtype%", "%configtype%", "%basictype%", "%premiumtype%",
"%inactivetype%", "%validtype%", "%emptyupdate%", "%emptyconfig%", "%scopetype%",
"%opentype%",
}
for _, pattern := range typePatterns {
typeModel.DestroyWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "type_id", OP: "like", Value: pattern},
},
})
}
// Clean users
userModel := model.Select("__yao.user")
@ -134,7 +149,8 @@ func cleanupTestData() {
userPatterns := []string{
"test-%", "test_%", "%testuser%", "%oauthtest%", "%oauthlist%",
"%oautherror%", "%deletetest%", "%roleuser%", "%typeuser%", "%scopeuser%",
"%erroruser%", "%noroleuser%", "%clearnouser%", "%integuser%",
"%erroruser%", "%noroleuser%", "%clearnouser%", "%integuser%", "%notypeuser%",
"%openuser%", "%clearnotypeuser%",
}
for _, pattern := range userPatterns {
userModel.DestroyWhere(model.QueryParam{
@ -148,6 +164,7 @@ func cleanupTestData() {
usernamePatterns := []string{
"testuser%", "%oauth_%", "%deletetest%", "%roleuser%", "%typeuser%",
"%scopeuser%", "%erroruser%", "%noroleuser%", "%clearnouser%", "%integuser%",
"%notypeuser%", "%openuser%", "%clearnotypeuser%",
}
for _, pattern := range usernamePatterns {
userModel.DestroyWhere(model.QueryParam{

View file

@ -177,6 +177,7 @@ type UserProvider interface {
ClearUserRole(ctx context.Context, userID string) error
GetUserType(ctx context.Context, userID string) (maps.MapStrAny, error)
SetUserType(ctx context.Context, userID string, typeID string) error
ClearUserType(ctx context.Context, userID string) error
ValidateUserScope(ctx context.Context, userID string, scopes []string) (bool, error)
// User MFA Management