Files
capsule/pkg/api/owner_status_list_test.go
2025-12-02 15:21:46 +01:00

356 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package api_test
import (
"math/rand"
"reflect"
"sort"
"testing"
"time"
"github.com/projectcapsule/capsule/pkg/api"
)
func slowIsOwner(o api.OwnerStatusListSpec, name string, groups []string) bool {
for _, owner := range o {
switch owner.Kind {
case api.UserOwner, api.ServiceAccountOwner:
if name == owner.Name {
return true
}
case api.GroupOwner:
for _, group := range groups {
if group == owner.Name {
return true
}
}
}
}
return false
}
// linearFind is the obvious, slow, but correct reference implementation.
func linearFind(o api.OwnerStatusListSpec, name string, kind api.OwnerKind) (api.CoreOwnerSpec, bool) {
for _, x := range o {
if x.Kind == kind && x.Name == name {
return x, true
}
}
return api.CoreOwnerSpec{}, false
}
// randomName generates a simple lowercase name of length n.
func randomName(rnd *rand.Rand, n int) string {
const letters = "abcdefghijklmnopqrstuvwxyz"
b := make([]byte, n)
for i := range b {
b[i] = letters[rnd.Intn(len(letters))]
}
return string(b)
}
func TestUpsert_AddsNewOwnerToEmptyList(t *testing.T) {
var list api.OwnerStatusListSpec
list.Upsert(api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "alice",
},
ClusterRoles: []string{"admin"},
})
if len(list) != 1 {
t.Fatalf("expected 1 owner, got %d", len(list))
}
got := list[0]
if got.Kind != api.UserOwner || got.Name != "alice" {
t.Fatalf("unexpected owner: %+v", got)
}
if !reflect.DeepEqual(got.ClusterRoles, []string{"admin"}) {
t.Fatalf("unexpected roles: %#v", got.ClusterRoles)
}
}
func TestUpsert_MergesClusterRolesForExistingOwner(t *testing.T) {
list := api.OwnerStatusListSpec{
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "alice",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
}
list.Upsert(api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "alice",
},
ClusterRoles: []string{"extra-sad"},
})
if len(list) != 1 {
t.Fatalf("expected 1 owner, got %d", len(list))
}
got := list[0]
if got.Kind != api.UserOwner || got.Name != "alice" {
t.Fatalf("unexpected owner: %+v", got)
}
// Roles should be union of both sets, order: existing roles first, then new ones
expected := []string{"admin", "capsule-namespace-deleter", "extra-sad"}
if !reflect.DeepEqual(got.ClusterRoles, expected) {
t.Fatalf("expected roles %v, got %v", expected, got.ClusterRoles)
}
}
func TestUpsert_DeduplicatesClusterRoles(t *testing.T) {
list := api.OwnerStatusListSpec{
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "alice",
},
ClusterRoles: []string{"admin", "viewer"},
},
}
list.Upsert(api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "alice",
},
ClusterRoles: []string{"viewer", "editor"},
})
if len(list) != 1 {
t.Fatalf("expected 1 owner, got %d", len(list))
}
got := list[0]
expected := []string{"admin", "viewer", "editor"}
if !reflect.DeepEqual(got.ClusterRoles, expected) {
t.Fatalf("expected roles %v, got %v", expected, got.ClusterRoles)
}
}
func TestUpsert_KeepsListSortedAndMergesIntoExistingInUnsortedInitialSlice(t *testing.T) {
// Start with an unsorted slice, as could come from API/server
list := api.OwnerStatusListSpec{
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "bob",
},
ClusterRoles: []string{"bob-role"},
},
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "alice",
},
ClusterRoles: []string{"admin"},
},
}
// Upsert another alice
list.Upsert(api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "alice",
},
ClusterRoles: []string{"extra"},
})
if len(list) != 2 {
t.Fatalf("expected 2 owners (alice, bob), got %d", len(list))
}
// Ensure sorted by Kind.Name: alice before bob
// (relies on ByKindAndName order)
sorted := make(api.OwnerStatusListSpec, len(list))
copy(sorted, list)
sort.Sort(api.GetByKindAndName(sorted))
if !reflect.DeepEqual(list, sorted) {
t.Fatalf("expected list to be sorted by kind+name, got %#v", list)
}
// Find alice and check roles
var alice *api.CoreOwnerSpec
for i := range list {
if list[i].Name == "alice" {
alice = &list[i]
break
}
}
if alice == nil {
t.Fatalf("alice not found in list")
}
expectedRoles := []string{"admin", "extra"}
if !reflect.DeepEqual(alice.ClusterRoles, expectedRoles) {
t.Fatalf("expected alice roles %v, got %v", expectedRoles, alice.ClusterRoles)
}
}
func TestGetByKindAndNameOrdering(t *testing.T) {
o := api.OwnerStatusListSpec{
api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "b", Kind: api.ServiceAccountOwner}},
api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "z", Kind: api.UserOwner}},
api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "a", Kind: api.GroupOwner}},
api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "a", Kind: api.UserOwner}},
}
// Sort using production ordering
got := append(api.OwnerStatusListSpec(nil), o...)
sort.Sort(api.GetByKindAndName(got))
// Manually sorted expectation using the same logic.
want := append(api.OwnerStatusListSpec(nil), o...)
sort.Slice(want, func(i, j int) bool {
if want[i].Kind.String() != want[j].Kind.String() {
return want[i].Kind.String() < want[j].Kind.String()
}
return want[i].Name < want[j].Name
})
if len(got) != len(want) {
t.Fatalf("length mismatch: got %d, want %d", len(got), len(want))
}
for i := range got {
if !reflect.DeepEqual(got[i], want[i]) {
t.Fatalf("ordering mismatch at %d: got %+v, want %+v", i, got[i], want[i])
}
}
}
func TestFindOwner_Randomized(t *testing.T) {
rnd := rand.New(rand.NewSource(42)) // fixed seed for deterministic test runs
ownerKinds := []api.OwnerKind{
api.GroupOwner,
api.UserOwner,
api.ServiceAccountOwner,
}
const (
numLists = 200
maxLength = 40
numLookupsPerList = 80
)
for listIdx := 0; listIdx < numLists; listIdx++ {
var list api.OwnerStatusListSpec
n := rnd.Intn(maxLength)
for i := 0; i < n; i++ {
k := ownerKinds[rnd.Intn(len(ownerKinds))]
list = append(list, api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: randomName(rnd, 3+rnd.Intn(4)), // length 36
Kind: k,
},
})
}
for lookupIdx := 0; lookupIdx < numLookupsPerList; lookupIdx++ {
var qName string
var qKind api.OwnerKind
if len(list) > 0 && rnd.Float64() < 0.6 {
// 60% of lookups: pick a real element, must be found
pick := list[rnd.Intn(len(list))]
qName = pick.Name
qKind = pick.Kind
} else {
// 40%: random query, may or may not exist
qName = randomName(rnd, 3+rnd.Intn(4))
qKind = ownerKinds[rnd.Intn(len(ownerKinds))]
}
listCopy := append(api.OwnerStatusListSpec(nil), list...)
gotOwner, gotFound := listCopy.FindOwner(qName, qKind)
wantOwner, wantFound := linearFind(list, qName, qKind)
if gotFound != wantFound {
t.Fatalf("list=%d lookup=%d: found mismatch for (%q,%v): got=%v, want=%v",
listIdx, lookupIdx, qName, qKind, gotFound, wantFound)
}
if gotFound && !reflect.DeepEqual(gotOwner, wantOwner) {
t.Fatalf("list=%d lookup=%d: owner mismatch for (%q,%v):\n got= %+v\nwant= %+v",
listIdx, lookupIdx, qName, qKind, gotOwner, wantOwner)
}
}
}
}
func TestIsOwner_RandomizedMatchesSlowImplementation(t *testing.T) {
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
ownerKinds := []api.OwnerKind{
api.UserOwner,
api.GroupOwner,
api.ServiceAccountOwner,
}
const (
numLists = 200
maxOwnersPerList = 30
numLookupsPerList = 80
maxGroupsPerUser = 10
)
for listIdx := 0; listIdx < numLists; listIdx++ {
// Generate a random owner list (possibly with duplicates).
var owners api.OwnerStatusListSpec
nOwners := rnd.Intn(maxOwnersPerList)
for i := 0; i < nOwners; i++ {
kind := ownerKinds[rnd.Intn(len(ownerKinds))]
owners = append(owners, api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: randomName(rnd, 3+rnd.Intn(4)), // length 36
Kind: kind,
},
})
}
for lookupIdx := 0; lookupIdx < numLookupsPerList; lookupIdx++ {
// Generate a random userName and groups,
// sometimes biased to hit existing owners/groups.
var userName string
var groups []string
// 50% of the time: pick an existing owner name as userName
if len(owners) > 0 && rnd.Float64() < 0.5 {
pick := owners[rnd.Intn(len(owners))]
userName = pick.Name
} else {
userName = randomName(rnd, 3+rnd.Intn(4))
}
// Random groups, sometimes including owner names
nGroups := rnd.Intn(maxGroupsPerUser)
for i := 0; i < nGroups; i++ {
if len(owners) > 0 && rnd.Float64() < 0.5 {
pick := owners[rnd.Intn(len(owners))]
groups = append(groups, pick.Name)
} else {
groups = append(groups, randomName(rnd, 3+rnd.Intn(4)))
}
}
got := owners.IsOwner(userName, groups)
want := slowIsOwner(owners, userName, groups)
if got != want {
t.Fatalf("list=%d lookup=%d: mismatch\n owners=%v\n user=%q\n groups=%v\n optimized=%v\n slow=%v",
listIdx, lookupIdx, owners, userName, groups, got, want)
}
}
}
}