- Enhanced the ACL structure to include a new FeatureManager field, allowing for better management of feature-related permissions. - Updated the New function to load the feature manager during ACL initialization, improving the overall functionality and logging success messages for better traceability.
410 lines
16 KiB
Go
410 lines
16 KiB
Go
package acl
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/yaoapp/gou/application"
|
|
)
|
|
|
|
// setupFeatureTest initializes test environment
|
|
func setupFeatureTest(t *testing.T) *FeatureManager {
|
|
// Set test application path
|
|
testApp := os.Getenv("YAO_TEST_APPLICATION")
|
|
if testApp == "" {
|
|
t.Skip("YAO_TEST_APPLICATION not set, skipping feature tests")
|
|
}
|
|
|
|
// Initialize application
|
|
app, err := application.OpenFromDisk(testApp)
|
|
if err != nil {
|
|
t.Fatalf("Failed to open application: %v", err)
|
|
}
|
|
application.Load(app)
|
|
|
|
// Load features
|
|
manager, err := LoadFeatures()
|
|
if err != nil {
|
|
t.Fatalf("Failed to load features: %v", err)
|
|
}
|
|
|
|
return manager
|
|
}
|
|
|
|
func TestLoadFeatures(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
assert.NotNil(t, manager)
|
|
}
|
|
|
|
func TestFeatures_SystemRoot(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Test system:root role with wildcard *:*:*
|
|
features := manager.Features("system:root")
|
|
assert.NotEmpty(t, features, "system:root should have features")
|
|
|
|
// Verify it has all features from all domains
|
|
assert.True(t, features["profile:read"], "should have profile:read")
|
|
assert.True(t, features["profile:edit"], "should have profile:edit")
|
|
assert.True(t, features["team:edit"], "should have team:edit")
|
|
assert.True(t, features["collections:create"], "should have collections:create")
|
|
assert.True(t, features["document:create"], "should have document:create")
|
|
assert.True(t, features["meta:edit"], "should have meta:edit")
|
|
}
|
|
|
|
func TestFeatures_OwnerFree(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Test owner:free role
|
|
features := manager.Features("owner:free")
|
|
assert.NotEmpty(t, features, "owner:free should have features")
|
|
|
|
// Should have profile:manage alias expanded
|
|
assert.True(t, features["profile:read"], "should have profile:read from profile:manage alias")
|
|
assert.True(t, features["profile:edit"], "should have profile:edit from profile:manage alias")
|
|
|
|
// Should have team:manage alias expanded
|
|
assert.True(t, features["team:edit"], "should have team:edit from team:manage alias")
|
|
assert.True(t, features["team:member:invite"], "should have team:member:invite from team:manage alias")
|
|
assert.True(t, features["team:member:remove"], "should have team:member:remove from team:manage alias")
|
|
|
|
// Should have collections:create
|
|
assert.True(t, features["collections:create"], "should have collections:create")
|
|
}
|
|
|
|
func TestFeatures_OwnerPro(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Test owner:pro role with user:full alias
|
|
features := manager.Features("owner:pro")
|
|
assert.NotEmpty(t, features, "owner:pro should have features")
|
|
|
|
// user:full includes profile:manage, team:manage, kb:manage
|
|
assert.True(t, features["profile:read"], "should have profile:read")
|
|
assert.True(t, features["profile:edit"], "should have profile:edit")
|
|
assert.True(t, features["team:edit"], "should have team:edit")
|
|
assert.True(t, features["collections:create"], "should have collections:create from kb:manage")
|
|
}
|
|
|
|
func TestFeaturesByDomain_User(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Query "user" domain - should include all user/* files
|
|
features := manager.FeaturesByDomain("system:root", "user")
|
|
assert.NotEmpty(t, features, "user domain should have features")
|
|
|
|
// Should include features from user/profile.yml
|
|
assert.True(t, features["profile:read"], "should have profile:read from user/profile")
|
|
assert.True(t, features["profile:edit"], "should have profile:edit from user/profile")
|
|
|
|
// Should include features from user/team.yml
|
|
assert.True(t, features["team:edit"], "should have team:edit from user/team")
|
|
|
|
// Should include features from user/team/settings.yml (nested)
|
|
assert.True(t, features["team:settings:view"], "should have team:settings:view from user/team/settings")
|
|
assert.True(t, features["team:settings:edit"], "should have team:settings:edit from user/team/settings")
|
|
|
|
// Should include features from user/team/members.yml (nested)
|
|
assert.True(t, features["team:members:list"], "should have team:members:list from user/team/members")
|
|
assert.True(t, features["team:members:add"], "should have team:members:add from user/team/members")
|
|
|
|
// Should NOT include kb features
|
|
assert.False(t, features["collections:create"], "should not have kb features")
|
|
}
|
|
|
|
func TestFeaturesByDomain_UserTeam(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Query "user/team" domain - should include user/team.yml (exact match) AND user/team/* files (prefix match)
|
|
features := manager.FeaturesByDomain("system:root", "user/team")
|
|
assert.NotEmpty(t, features, "user/team domain should have features")
|
|
|
|
// Should NOT include user/profile features
|
|
assert.False(t, features["profile:read"], "should not have profile:read")
|
|
|
|
// Should include user/team.yml features (exact match on domain="user/team")
|
|
assert.True(t, features["team:edit"], "should have team:edit from user/team.yml (exact match)")
|
|
assert.True(t, features["team:member:invite"], "should have team:member:invite from user/team.yml")
|
|
|
|
// Should include features from user/team/settings.yml (prefix match)
|
|
assert.True(t, features["team:settings:view"], "should have team:settings:view from user/team/settings")
|
|
assert.True(t, features["team:settings:edit"], "should have team:settings:edit from user/team/settings")
|
|
|
|
// Should include features from user/team/members.yml (prefix match)
|
|
assert.True(t, features["team:members:list"], "should have team:members:list from user/team/members")
|
|
assert.True(t, features["team:members:add"], "should have team:members:add from user/team/members")
|
|
}
|
|
|
|
func TestFeaturesByDomain_UserProfile(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Query "user/profile" domain - should only include user/profile.yml
|
|
features := manager.FeaturesByDomain("system:root", "user/profile")
|
|
assert.NotEmpty(t, features, "user/profile domain should have features")
|
|
|
|
// Should include features from user/profile.yml
|
|
assert.True(t, features["profile:read"], "should have profile:read")
|
|
assert.True(t, features["profile:edit"], "should have profile:edit")
|
|
|
|
// Should NOT include team features
|
|
assert.False(t, features["team:edit"], "should not have team:edit")
|
|
assert.False(t, features["team:settings:view"], "should not have team:settings:view")
|
|
}
|
|
|
|
func TestFeaturesByDomain_KB(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Query "kb" domain - should include all kb/* files
|
|
features := manager.FeaturesByDomain("system:root", "kb")
|
|
assert.NotEmpty(t, features, "kb domain should have features")
|
|
|
|
// Should include features from kb/collections.yml
|
|
assert.True(t, features["collections:create"], "should have collections:create")
|
|
|
|
// Should include features from kb/collections/document.yml
|
|
assert.True(t, features["document:create"], "should have document:create")
|
|
assert.True(t, features["document:edit"], "should have document:edit")
|
|
assert.True(t, features["document:delete"], "should have document:delete")
|
|
|
|
// Should include features from kb/collections/advanced/meta.yml (deep nested)
|
|
assert.True(t, features["meta:edit"], "should have meta:edit from kb/collections/advanced/meta")
|
|
assert.True(t, features["meta:view"], "should have meta:view from kb/collections/advanced/meta")
|
|
assert.True(t, features["meta:export"], "should have meta:export from kb/collections/advanced/meta")
|
|
|
|
// Should NOT include user features
|
|
assert.False(t, features["profile:read"], "should not have user features")
|
|
}
|
|
|
|
func TestFeaturesByDomain_KBCollections(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Query "kb/collections" domain - should include kb/collections.yml and kb/collections/* files
|
|
features := manager.FeaturesByDomain("system:root", "kb/collections")
|
|
assert.NotEmpty(t, features, "kb/collections domain should have features")
|
|
|
|
// Should include features from kb/collections.yml (exact match)
|
|
assert.True(t, features["collections:create"], "should have collections:create from kb/collections")
|
|
|
|
// Should include features from kb/collections/document.yml (nested)
|
|
assert.True(t, features["document:create"], "should have document:create")
|
|
assert.True(t, features["document:edit"], "should have document:edit")
|
|
|
|
// Should include features from kb/collections/advanced/meta.yml (deep nested)
|
|
assert.True(t, features["meta:edit"], "should have meta:edit")
|
|
assert.True(t, features["meta:view"], "should have meta:view")
|
|
}
|
|
|
|
func TestFeaturesByDomain_KBCollectionsAdvanced(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Query "kb/collections/advanced" domain - should include kb/collections/advanced/* files
|
|
features := manager.FeaturesByDomain("system:root", "kb/collections/advanced")
|
|
assert.NotEmpty(t, features, "kb/collections/advanced domain should have features")
|
|
|
|
// Should include features from kb/collections/advanced/meta.yml
|
|
assert.True(t, features["meta:edit"], "should have meta:edit")
|
|
assert.True(t, features["meta:view"], "should have meta:view")
|
|
assert.True(t, features["meta:export"], "should have meta:export")
|
|
|
|
// Should NOT include kb/collections.yml features
|
|
assert.False(t, features["collections:create"], "should not have collections:create")
|
|
|
|
// Should NOT include kb/collections/document.yml features
|
|
assert.False(t, features["document:create"], "should not have document:create")
|
|
}
|
|
|
|
func TestDomainFeatures(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Test exact domain match - user/profile
|
|
features := manager.DomainFeatures("user/profile")
|
|
assert.NotEmpty(t, features, "user/profile domain should have features")
|
|
assert.True(t, features["profile:read"], "should have profile:read")
|
|
assert.True(t, features["profile:edit"], "should have profile:edit")
|
|
|
|
// Should NOT include nested domains
|
|
assert.False(t, features["team:edit"], "should not include user/team features")
|
|
assert.False(t, features["team:settings:view"], "should not include user/team/settings features")
|
|
}
|
|
|
|
func TestDomains(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
domains := manager.Domains()
|
|
assert.NotEmpty(t, domains, "should have domains")
|
|
|
|
// Check expected domains exist
|
|
expectedDomains := []string{
|
|
"user/profile",
|
|
"user/team",
|
|
"user/team/settings",
|
|
"user/team/members",
|
|
"kb/collections",
|
|
"kb/collections/document",
|
|
"kb/collections/advanced/meta",
|
|
}
|
|
|
|
domainMap := make(map[string]bool)
|
|
for _, d := range domains {
|
|
domainMap[d] = true
|
|
}
|
|
|
|
for _, expected := range expectedDomains {
|
|
assert.True(t, domainMap[expected], "should have domain: %s", expected)
|
|
}
|
|
}
|
|
|
|
func TestDefinition(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Test feature definition lookup
|
|
def := manager.Definition("profile:read")
|
|
assert.NotNil(t, def, "should find profile:read definition")
|
|
assert.Equal(t, "Read own profile", def.Description, "description should match")
|
|
|
|
// Test nested domain feature
|
|
def = manager.Definition("team:settings:view")
|
|
assert.NotNil(t, def, "should find team:settings:view definition")
|
|
assert.Equal(t, "View team settings", def.Description, "description should match")
|
|
|
|
// Test deep nested feature
|
|
def = manager.Definition("meta:edit")
|
|
assert.NotNil(t, def, "should find meta:edit definition")
|
|
assert.Equal(t, "Edit document metadata", def.Description, "description should match")
|
|
|
|
// Test non-existent feature
|
|
def = manager.Definition("nonexistent:feature")
|
|
assert.Nil(t, def, "should return nil for non-existent feature")
|
|
}
|
|
|
|
func TestAliasExpansion(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Test that aliases are properly expanded
|
|
features := manager.Features("owner:free")
|
|
|
|
// profile:manage should expand to profile:read + profile:edit
|
|
assert.True(t, features["profile:read"], "profile:manage alias should include profile:read")
|
|
assert.True(t, features["profile:edit"], "profile:manage alias should include profile:edit")
|
|
|
|
// team:manage should expand to multiple team features
|
|
assert.True(t, features["team:edit"], "team:manage alias should include team:edit")
|
|
assert.True(t, features["team:member:invite"], "team:manage alias should include team:member:invite")
|
|
assert.True(t, features["team:member:robot:create"], "team:manage alias should include team:member:robot:create")
|
|
assert.True(t, features["team:member:robot:edit"], "team:manage alias should include team:member:robot:edit")
|
|
assert.True(t, features["team:member:remove"], "team:manage alias should include team:member:remove")
|
|
}
|
|
|
|
func TestNestedAliasExpansion(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Test nested alias: user:full -> profile:manage, team:manage, kb:manage
|
|
features := manager.Features("owner:pro")
|
|
|
|
// Should expand all nested aliases
|
|
assert.True(t, features["profile:read"], "should have profile:read from profile:manage")
|
|
assert.True(t, features["profile:edit"], "should have profile:edit from profile:manage")
|
|
assert.True(t, features["team:edit"], "should have team:edit from team:manage")
|
|
assert.True(t, features["team:member:invite"], "should have team:member:invite from team:manage")
|
|
assert.True(t, features["collections:create"], "should have collections:create from kb:manage")
|
|
}
|
|
|
|
func TestEmptyRole(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Test non-existent role
|
|
features := manager.Features("nonexistent:role")
|
|
assert.Empty(t, features, "non-existent role should return empty map")
|
|
|
|
// Test by domain with non-existent role
|
|
features = manager.FeaturesByDomain("nonexistent:role", "user")
|
|
assert.Empty(t, features, "non-existent role should return empty map for domain query")
|
|
}
|
|
|
|
func TestEmptyDomain(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Test non-existent domain
|
|
features := manager.FeaturesByDomain("system:root", "nonexistent")
|
|
assert.Empty(t, features, "non-existent domain should return empty map")
|
|
|
|
// Test DomainFeatures with non-existent domain
|
|
features = manager.DomainFeatures("nonexistent")
|
|
assert.Empty(t, features, "non-existent domain should return empty map")
|
|
}
|
|
|
|
func TestFeatureMapReturnType(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Verify return type is map[string]bool for O(1) lookups
|
|
features := manager.Features("owner:free")
|
|
|
|
// Should be able to check existence with simple map lookup
|
|
if features["profile:read"] {
|
|
// Feature exists
|
|
assert.True(t, true)
|
|
} else {
|
|
t.Error("profile:read should exist for owner:free")
|
|
}
|
|
|
|
// Check non-existent feature
|
|
if features["nonexistent:feature"] {
|
|
t.Error("nonexistent:feature should not exist")
|
|
}
|
|
}
|
|
|
|
func TestHierarchicalQueryBehavior(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
|
|
// Verify hierarchical behavior: querying parent includes children
|
|
userFeatures := manager.FeaturesByDomain("system:root", "user")
|
|
userTeamFeatures := manager.FeaturesByDomain("system:root", "user/team")
|
|
userProfileFeatures := manager.FeaturesByDomain("system:root", "user/profile")
|
|
|
|
// user should include more features than user/team and user/profile
|
|
assert.Greater(t, len(userFeatures), len(userTeamFeatures), "user should have more features than user/team")
|
|
assert.Greater(t, len(userFeatures), len(userProfileFeatures), "user should have more features than user/profile")
|
|
|
|
// user/team should NOT include user/profile features
|
|
assert.True(t, userProfileFeatures["profile:read"], "user/profile should have profile:read")
|
|
assert.False(t, userTeamFeatures["profile:read"], "user/team should NOT have profile:read")
|
|
|
|
// user should include both
|
|
assert.True(t, userFeatures["profile:read"], "user should have profile:read")
|
|
assert.True(t, userFeatures["team:settings:view"], "user should have team:settings:view")
|
|
}
|
|
|
|
func TestConvenienceMethods(t *testing.T) {
|
|
manager := setupFeatureTest(t)
|
|
ctx := context.Background()
|
|
|
|
// Note: These tests will skip if role manager is not properly initialized
|
|
// In production, role manager would be initialized with a real provider
|
|
|
|
// Test FeaturesForUser (will fail if role manager not initialized, which is expected in test)
|
|
_, err := manager.FeaturesForUser(ctx, "test-user-123")
|
|
// We expect an error because role manager might not be initialized
|
|
if err != nil {
|
|
assert.Contains(t, err.Error(), "role manager", "should indicate role manager issue")
|
|
}
|
|
|
|
// Test FeaturesForUserByDomain
|
|
_, err = manager.FeaturesForUserByDomain(ctx, "test-user-123", "user")
|
|
if err != nil {
|
|
assert.Contains(t, err.Error(), "role manager", "should indicate role manager issue")
|
|
}
|
|
|
|
// Test FeaturesForTeamUser
|
|
_, err = manager.FeaturesForTeamUser(ctx, "test-team-456", "test-user-123")
|
|
if err != nil {
|
|
assert.Contains(t, err.Error(), "role manager", "should indicate role manager issue")
|
|
}
|
|
|
|
// Test FeaturesForTeamUserByDomain
|
|
_, err = manager.FeaturesForTeamUserByDomain(ctx, "test-team-456", "test-user-123", "user")
|
|
if err != nil {
|
|
assert.Contains(t, err.Error(), "role manager", "should indicate role manager issue")
|
|
}
|
|
}
|