Files
capsule/pkg/api/users_list_test.go
Oliver Bähler a6b830b1af feat: add ruleset api(#1844)
* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix(config): remove usergroups default

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix(config): remove usergroups default

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* sec(ghsa-2ww6-hf35-mfjm): intercept namespace subresource

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2026-01-27 14:28:48 +01:00

326 lines
8.4 KiB
Go
Raw Permalink 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-2026 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 linearFindUser(list api.UserListSpec, name string, kind api.OwnerKind) (api.UserSpec, bool) {
for _, u := range list {
if u.Kind == kind && u.Name == name {
return u, true
}
}
return api.UserSpec{}, false
}
func slowIsPresent(u api.UserListSpec, name string, groups []string) bool {
for _, user := range u {
switch user.Kind {
case api.UserOwner, api.ServiceAccountOwner:
if name == user.Name {
return true
}
case api.GroupOwner:
for _, group := range groups {
if group == user.Name {
return true
}
}
}
}
return false
}
func TestByKindNameOrdering_UserListSpec(t *testing.T) {
u := api.UserListSpec{
api.UserSpec{Name: "b", Kind: api.ServiceAccountOwner},
api.UserSpec{Name: "z", Kind: api.UserOwner},
api.UserSpec{Name: "a", Kind: api.GroupOwner},
api.UserSpec{Name: "a", Kind: api.UserOwner},
}
// Sort using production ordering
got := append(api.UserListSpec(nil), u...)
sort.Sort(api.ByKindName(got))
// Manually sorted expectation using the same logic.
want := append(api.UserListSpec(nil), u...)
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 TestFindUser_Randomized(t *testing.T) {
rnd := rand.New(rand.NewSource(42))
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.UserListSpec
n := rnd.Intn(maxLength)
for i := 0; i < n; i++ {
k := ownerKinds[rnd.Intn(len(ownerKinds))]
list = append(list, 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.UserListSpec(nil), list...) // FindUser sorts in-place
gotUser, gotFound := listCopy.FindUser(qName, qKind)
wantUser, wantFound := linearFindUser(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(gotUser, wantUser) {
t.Fatalf("list=%d lookup=%d: user mismatch for (%q,%v):\n got= %+v\nwant= %+v",
listIdx, lookupIdx, qName, qKind, gotUser, wantUser)
}
}
}
}
func TestIsPresent_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 user list (possibly with duplicates).
var users api.UserListSpec
nOwners := rnd.Intn(maxOwnersPerList)
for i := 0; i < nOwners; i++ {
kind := ownerKinds[rnd.Intn(len(ownerKinds))]
users = append(users, 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(users) > 0 && rnd.Float64() < 0.5 {
pick := users[rnd.Intn(len(users))]
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(users) > 0 && rnd.Float64() < 0.5 {
pick := users[rnd.Intn(len(users))]
groups = append(groups, pick.Name)
} else {
groups = append(groups, randomName(rnd, 3+rnd.Intn(4)))
}
}
got := users.IsPresent(userName, groups)
want := slowIsPresent(users, userName, groups)
if got != want {
t.Fatalf("list=%d lookup=%d: mismatch\n users=%v\n user=%q\n groups=%v\n optimized=%v\n slow=%v",
listIdx, lookupIdx, users, userName, groups, got, want)
}
}
}
}
func TestGetByKinds_Basic(t *testing.T) {
users := api.UserListSpec{
api.UserSpec{Name: "alice", Kind: api.UserOwner},
api.UserSpec{Name: "svc-1", Kind: api.ServiceAccountOwner},
api.UserSpec{Name: "team-a", Kind: api.GroupOwner},
api.UserSpec{Name: "bob", Kind: api.UserOwner},
api.UserSpec{Name: "team-b", Kind: api.GroupOwner},
}
eqStrings := func(got, want []string) bool {
if got == nil && want == nil {
return true
}
if len(got) != len(want) {
return false
}
sort.Strings(got)
sort.Strings(want)
for i := range got {
if got[i] != want[i] {
return false
}
}
return true
}
// Single kind: UserOwner
gotUsers := users.GetByKinds([]api.OwnerKind{api.UserOwner})
wantUsers := []string{"alice", "bob"}
if !eqStrings(gotUsers, wantUsers) {
t.Fatalf("GetByKinds([UserOwner]) = %v, want %v", gotUsers, wantUsers)
}
// Single kind: GroupOwner
gotGroups := users.GetByKinds([]api.OwnerKind{api.GroupOwner})
wantGroups := []string{"team-a", "team-b"}
if !eqStrings(gotGroups, wantGroups) {
t.Fatalf("GetByKinds([GroupOwner]) = %v, want %v", gotGroups, wantGroups)
}
// Multiple kinds: UserOwner + ServiceAccountOwner
gotUsersAndSAs := users.GetByKinds([]api.OwnerKind{api.UserOwner, api.ServiceAccountOwner})
wantUsersAndSAs := []string{"alice", "bob", "svc-1"}
if !eqStrings(gotUsersAndSAs, wantUsersAndSAs) {
t.Fatalf("GetByKinds([UserOwner,ServiceAccountOwner]) = %v, want %v",
gotUsersAndSAs, wantUsersAndSAs)
}
// No kinds → nil
gotNone := users.GetByKinds(nil)
if gotNone != nil {
t.Fatalf("GetByKinds(nil) = %v, want nil", gotNone)
}
// Kind not present at all
gotUnknown := users.GetByKinds([]api.OwnerKind{api.OwnerKind("does-not-exist")})
if gotUnknown != nil {
t.Fatalf("GetByKinds([unknown]) = %v, want nil", gotUnknown)
}
}
func TestGetByKinds_Randomized(t *testing.T) {
rnd := rand.New(rand.NewSource(123))
ownerKinds := []api.OwnerKind{
api.UserOwner,
api.GroupOwner,
api.ServiceAccountOwner,
}
const (
numLists = 200
maxOwnersPerList = 50
)
for listIdx := 0; listIdx < numLists; listIdx++ {
var users api.UserListSpec
n := rnd.Intn(maxOwnersPerList)
for i := 0; i < n; i++ {
k := ownerKinds[rnd.Intn(len(ownerKinds))]
users = append(users, api.UserSpec{
Name: randomName(rnd, 3+rnd.Intn(4)), // reuse your helper
Kind: k,
})
}
// Try several random kind-subsets per list
for subsetIdx := 0; subsetIdx < 10; subsetIdx++ {
// Build a random subset of kinds
var kinds []api.OwnerKind
for _, k := range ownerKinds {
if rnd.Float64() < 0.5 {
kinds = append(kinds, k)
}
}
got := users.GetByKinds(kinds)
// Reference implementation: filter + sort
kindSet := make(map[api.OwnerKind]struct{}, len(kinds))
for _, k := range kinds {
kindSet[k] = struct{}{}
}
var want []string
if len(kinds) > 0 {
for _, u := range users {
if _, ok := kindSet[u.Kind]; ok {
want = append(want, u.Name)
}
}
}
if len(want) == 0 {
if got != nil {
t.Fatalf("list=%d subset=%d: expected nil, got %v (kinds=%v, users=%v)",
listIdx, subsetIdx, got, kinds, users)
}
continue
}
sort.Strings(want)
sort.Strings(got)
if !reflect.DeepEqual(got, want) {
t.Fatalf("list=%d subset=%d: GetByKinds mismatch\n kinds=%v\n got= %v\n want= %v",
listIdx, subsetIdx, kinds, got, want)
}
}
}
}