// Copyright (c) 2026 Sidero Labs, Inc. // // Use of this software is governed by the Business Source License // included in the LICENSE file. package scope_test import ( "slices" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/siderolabs/omni/internal/pkg/auth/scope" ) func TestScopeCheck(t *testing.T) { for _, tt := range []struct { //nolint:govet name string actorScopes []scope.Scope requiredScopes []scope.Scope expectedError string }{ { name: "empty", }, { name: "actor okay", actorScopes: []scope.Scope{ scope.ClusterAny, scope.UserAny, scope.MachineAny, }, requiredScopes: []scope.Scope{ scope.New(scope.ObjectCluster, scope.ActionRead, scope.PerspectiveNone), scope.New(scope.ObjectUser, scope.ActionDestroy, scope.PerspectiveNone), }, }, { name: "missing scope", actorScopes: []scope.Scope{ scope.UserAny, scope.MachineAny, }, requiredScopes: []scope.Scope{ scope.New(scope.ObjectCluster, scope.ActionRead, scope.PerspectiveNone), scope.New(scope.ObjectUser, scope.ActionDestroy, scope.PerspectiveNone), }, expectedError: "missing required scope: cluster:read", }, } { t.Run(tt.name, func(t *testing.T) { actorScopes := scope.NewScopes(tt.actorScopes...) err := actorScopes.Check(tt.requiredScopes...) if tt.expectedError != "" { require.Error(t, err) assert.EqualError(t, err, tt.expectedError) } else { require.NoError(t, err) } }) } } func TestParseScopes(t *testing.T) { scopes := scope.NewScopes(scope.ClusterAny, scope.MachineAny) assert.Equal(t, []string{"cluster:*", "machine:*"}, scopes.Strings()) parsedScopes, err := scope.ParseScopes(scopes.Strings()) require.NoError(t, err) assert.Equal(t, scopes, parsedScopes) } func TestIntersection(t *testing.T) { scopeUserRead := scope.New(scope.ObjectUser, scope.ActionRead, scope.PerspectiveNone) scopeClusterRead := scope.New(scope.ObjectCluster, scope.ActionRead, scope.PerspectiveNone) scopeClusterCreate := scope.New(scope.ObjectCluster, scope.ActionCreate, scope.PerspectiveNone) scopeClusterUpgrade := scope.New(scope.ObjectCluster, scope.ActionModify, scope.PerspectiveKubernetesUpgrade) scopeMachineDestroy := scope.New(scope.ObjectMachine, scope.ActionDestroy, scope.PerspectiveNone) testCases := []struct { name string scopes1 scope.Scopes scopes2 scope.Scopes expected scope.Scopes }{ { name: "with-any", scopes1: scope.NewScopes(scope.ClusterAny, scope.MachineAny), scopes2: scope.NewScopes(scope.ClusterAny, scope.MachineAny, scope.UserAny), expected: scope.NewScopes(scope.ClusterAny, scope.MachineAny), }, { name: "with-empty", scopes1: scope.NewScopes(scopeUserRead, scope.MachineAny), scopes2: scope.NewScopes(), expected: scope.NewScopes(), }, { name: "mixed-1", scopes1: scope.NewScopes(scopeClusterCreate, scope.MachineAny), scopes2: scope.NewScopes(scope.ClusterAny, scope.MachineAny), expected: scope.NewScopes(scopeClusterCreate, scope.MachineAny), }, { name: "mixed-2", scopes1: scope.NewScopes(scope.UserDefaultScopes...), scopes2: scope.NewScopes(scopeClusterRead), expected: scope.NewScopes(scopeClusterRead), }, { name: "mixed-inverted", scopes2: scope.NewScopes(scope.ClusterAny, scope.MachineAny), scopes1: scope.NewScopes(scopeClusterCreate, scope.MachineAny), expected: scope.NewScopes(scopeClusterCreate, scope.MachineAny), }, { name: "complex", scopes1: scope.NewScopes(scopeUserRead, scopeMachineDestroy, scopeClusterUpgrade), scopes2: scope.NewScopes(scope.ClusterAny, scope.MachineAny), expected: scope.NewScopes(scopeMachineDestroy, scopeClusterUpgrade), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { intersection := tc.scopes1.Intersect(tc.scopes2) expectedStrs := tc.expected.Strings() actualStrs := intersection.Strings() slices.Sort(expectedStrs) slices.Sort(actualStrs) assert.Equal(t, expectedStrs, actualStrs) }) } }