Ctrl K
ComunidadGuías…

internal/resources/namespacescope_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package resources

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"sync"
	"testing"
	"time"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/dynamic/fake"
	k8sfake "k8s.io/client-go/kubernetes/fake"
	k8stesting "k8s.io/client-go/testing"

	"github.com/sophotechlabs/spinoza/internal/access"
	"github.com/sophotechlabs/spinoza/internal/api"
)

type listCounter struct {
	mu          sync.Mutex
	byNamespace map[string]int
}

func (c *listCounter) seen(namespace string) int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.byNamespace[namespace]
}

func namespaceBoundClient(t *testing.T, allowed map[string]bool, objs ...runtime.Object) (*fake.FakeDynamicClient, *listCounter) {
	t.Helper()
	dyn := newClient(t, objs...)
	counts := &listCounter{byNamespace: map[string]int{}}
	dyn.PrependReactor("list", "deployments", func(action k8stesting.Action) (bool, runtime.Object, error) {
		counts.mu.Lock()
		counts.byNamespace[action.GetNamespace()]++
		counts.mu.Unlock()
		if allowed[action.GetNamespace()] {
			return false, nil, nil
		}
		return true, nil, apierrors.NewForbidden(depGVR.GroupResource(), "", errors.New("deployments.apps is forbidden: this account reads named namespaces only"))
	})
	return dyn, counts
}

func scopedManager(t *testing.T, dyn *fake.FakeDynamicClient) *Manager {
	t.Helper()
	mgr, cancel := newManager(t, dyn)
	t.Cleanup(cancel)
	mgr.syncTimeout = time.Second
	return mgr
}

func TestANamespaceOnlyCredentialCanBrowseItsNamespace(t *testing.T) {
	dyn, counts := namespaceBoundClient(t, map[string]bool{"review": true}, newDeployment("review", "first"))
	mgr := scopedManager(t, dyn)

	sub, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "review", 0, nil)
	if err != nil {
		t.Fatalf("a valid namespace list succeeded but the subscription failed: %v", err)
	}
	t.Cleanup(sub.Close)

	if len(sub.Rows) != 1 || sub.Rows[0].Name != "first" {
		t.Fatalf("rows = %+v, want the one deployment the account may read", sub.Rows)
	}
	if counts.seen("") != 1 {
		t.Fatalf("cluster-wide lists = %d, want exactly one attempt before scoping down", counts.seen(""))
	}
	if counts.seen("review") != 1 {
		t.Fatalf("namespace lists = %d, want one informer for the namespace", counts.seen("review"))
	}

	_, err = dyn.Resource(depGVR).Namespace("review").Create(context.Background(), newDeployment("review", "second"), metav1.CreateOptions{})
	if err != nil {
		t.Fatal(err)
	}
	ev := recvEvent(t, sub.Events)
	if ev.Kind != "added" || ev.Row.Name != "second" {
		t.Fatalf("event = %+v, want the deployment that just appeared", ev)
	}
}

func TestANamespaceOnlyCredentialIsStillRefusedElsewhere(t *testing.T) {
	dyn, _ := namespaceBoundClient(t, map[string]bool{"review": true})
	mgr := scopedManager(t, dyn)

	_, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "other", 0, nil)

	if !apierrors.IsForbidden(err) {
		t.Fatalf("error = %v, want the cluster's own refusal for the other namespace", err)
	}
}

func TestANamespaceOnlyCredentialMustPickANamespaceForTheWholeCluster(t *testing.T) {
	dyn, _ := namespaceBoundClient(t, map[string]bool{"review": true})
	mgr := scopedManager(t, dyn)

	_, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "", 0, nil)

	if !errors.Is(err, ErrNamespaceNeeded) {
		t.Fatalf("error = %v, want ErrNamespaceNeeded", err)
	}
	want := "your account cannot read this kind across every namespace; pick one namespace: Deployment: "
	if !strings.HasPrefix(err.Error(), want) {
		t.Fatalf("error = %q, want it to start with %q", err.Error(), want)
	}
	if !apierrors.IsForbidden(err) || !strings.Contains(err.Error(), "forbidden") {
		t.Fatalf("error = %q, want the cluster's own denial kept inside it", err.Error())
	}
}

func TestAKindInNoNamespaceKeepsTheClustersOwnRefusal(t *testing.T) {
	dyn := newClient(t)
	dyn.PrependReactor("list", "nodes", func(k8stesting.Action) (bool, runtime.Object, error) {
		return true, nil, apierrors.NewForbidden(nodeGVR.GroupResource(), "", errors.New("nodes is forbidden"))
	})
	mgr := scopedManager(t, dyn)

	_, err := mgr.Subscribe(t.Context(), "", "v1", "nodes", "", 0, nil)

	if !apierrors.IsForbidden(err) || errors.Is(err, ErrNamespaceNeeded) {
		t.Fatalf("error = %v, want the plain refusal, since no namespace could help", err)
	}
}

func TestScopedStreamsAreSharedPerNamespaceNotPerSubscriber(t *testing.T) {
	dyn, counts := namespaceBoundClient(t, map[string]bool{"a": true, "b": true},
		newDeployment("a", "one"), newDeployment("b", "two"))
	mgr := scopedManager(t, dyn)

	for _, namespace := range []string{"a", "b", "a"} {
		sub, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", namespace, 0, nil)
		if err != nil {
			t.Fatalf("subscribe %s: %v", namespace, err)
		}
		t.Cleanup(sub.Close)
	}

	if counts.seen("a") != 1 || counts.seen("b") != 1 {
		t.Fatalf("lists = a:%d b:%d, want one informer per namespace", counts.seen("a"), counts.seen("b"))
	}
	if streamCount(mgr) != 2 {
		t.Fatalf("streams = %d, want one per namespace the account can read", streamCount(mgr))
	}
}

func TestAScopedStreamIsNotOfferedAsTheWholeKind(t *testing.T) {
	dyn, _ := namespaceBoundClient(t, map[string]bool{"review": true}, newDeployment("review", "first"))
	mgr := scopedManager(t, dyn)
	sub, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "review", 0, nil)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(sub.Close)

	if _, offered := mgr.syncedTypes()[gvrKey(depGVR)]; offered {
		t.Fatal("a namespace-scoped informer was offered as a view of the whole kind")
	}
}

func TestEqualTimestampsSelectTheSameWindowWhateverTheInputOrder(t *testing.T) {
	first := newDeployment("review", "first")
	second := newDeployment("review", "second")
	stamp := metav1.NewTime(time.Date(2026, 9, 11, 10, 0, 0, 0, time.UTC))
	first.SetCreationTimestamp(stamp)
	second.SetCreationTimestamp(stamp)

	left := newestFirst("Deployment", []*unstructured.Unstructured{first, second}, 1)
	right := newestFirst("Deployment", []*unstructured.Unstructured{second, first}, 1)

	if left[0].GetUID() != right[0].GetUID() {
		t.Fatalf("identical objects selected different windows: %s versus %s", left[0].GetName(), right[0].GetName())
	}
	if left[0].GetName() != "first" {
		t.Fatalf("window = %s, want the alphabetically first of two equal-time objects", left[0].GetName())
	}
}

func TestAWiderWindowKeepsTheNarrowerOneAsItsPrefix(t *testing.T) {
	stamp := metav1.NewTime(time.Date(2026, 9, 11, 10, 0, 0, 0, time.UTC))
	held := make([]*unstructured.Unstructured, 0, 5)
	for _, name := range []string{"e", "c", "a", "d", "b"} {
		obj := newDeployment("review", name)
		obj.SetCreationTimestamp(stamp)
		held = append(held, obj)
	}

	narrow := newestFirst("Deployment", held, 2)
	wide := newestFirst("Deployment", held, 4)

	for at, obj := range narrow {
		if wide[at].GetName() != obj.GetName() {
			t.Fatalf("position %d = %s in the wide window, %s in the narrow one", at, wide[at].GetName(), obj.GetName())
		}
	}
}

func eventSeenAt(name, seen string) *unstructured.Unstructured {
	return &unstructured.Unstructured{Object: map[string]any{
		"apiVersion":    "v1",
		"kind":          "Event",
		"metadata":      map[string]any{"name": name, "namespace": "review", "uid": "uid-" + name},
		"lastTimestamp": seen,
	}}
}

func TestEventsThatWereLastSeenTogetherBreakTheTieByName(t *testing.T) {
	seen := "2026-09-11T10:00:00Z"
	late := eventSeenAt("late", seen)
	early := eventSeenAt("early", seen)

	left := newestFirst(eventKind, []*unstructured.Unstructured{late, early}, 1)
	right := newestFirst(eventKind, []*unstructured.Unstructured{early, late}, 1)

	if left[0].GetName() != "early" || right[0].GetName() != "early" {
		t.Fatalf("windows = %s and %s, want the same event from either order", left[0].GetName(), right[0].GetName())
	}
}

func adviceManager(t *testing.T, perms *access.Service, namespaces ...string) *Manager {
	t.Helper()
	dyn, _ := namespaceBoundClient(t, map[string]bool{})
	objects := make([]runtime.Object, 0, len(namespaces))
	for _, name := range namespaces {
		objects = append(objects, meta("", "v1", "Namespace", "", name))
	}
	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)
	mgr := NewManager(ctx, Deps{
		Dynamic:     dyn,
		Metadata:    fakeMeta(t, objects...),
		Perms:       perms,
		Categories:  []api.Category{{Name: "Workloads"}},
		Descriptors: testDescs(),
		Limits:      Limits{IdleGrace: time.Millisecond},
	})
	mgr.syncTimeout = time.Second
	return mgr
}

func TestAWholeClusterRefusalNamesTheNamespacesThatWouldWork(t *testing.T) {
	mgr := adviceManager(t, boundTo("review", "staging"), "other", "review", "staging")

	_, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "", 0, nil)

	if !errors.Is(err, ErrNamespaceNeeded) {
		t.Fatalf("error = %v, want ErrNamespaceNeeded", err)
	}
	if !strings.Contains(err.Error(), "Deployment, readable in review, staging: ") {
		t.Fatalf("error = %q, want the namespaces that would work named", err.Error())
	}
	if !apierrors.IsForbidden(err) {
		t.Fatalf("error = %q, want the cluster's own denial kept inside it", err.Error())
	}
}

func TestAWholeClusterRefusalSaysWhenNoNamespaceWouldHelp(t *testing.T) {
	mgr := adviceManager(t, boundTo(), "other", "review")

	_, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "", 0, nil)

	if !errors.Is(err, ErrKindUnreadable) || errors.Is(err, ErrNamespaceNeeded) {
		t.Fatalf("error = %v, want ErrKindUnreadable rather than advice to pick a namespace", err)
	}
	want := "your account cannot read this kind in any namespace spinoza knows of: Deployment: "
	if !strings.HasPrefix(err.Error(), want) {
		t.Fatalf("error = %q, want it to start with %q", err.Error(), want)
	}
	if !apierrors.IsForbidden(err) {
		t.Fatalf("error = %q, want the cluster's own denial kept inside it", err.Error())
	}
}

func TestAWholeClusterRefusalKeepsTheAdviceWhenTheAnswerIsUnknown(t *testing.T) {
	cs := k8sfake.NewClientset()
	cs.PrependReactor("create", "selfsubjectaccessreviews", func(k8stesting.Action) (bool, runtime.Object, error) {
		return true, nil, errors.New("authorization webhook timed out")
	})
	mgr := adviceManager(t, access.New(cs), "other", "review")

	_, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "", 0, nil)

	if !errors.Is(err, ErrNamespaceNeeded) {
		t.Fatalf("error = %v, want the advice kept when no namespace could be ruled out", err)
	}
}

func TestAWholeClusterRefusalChecksOnlyTheFirstNamespacesItKnows(t *testing.T) {
	names := make([]string, 0, adviceLimit+1)
	for at := range adviceLimit + 1 {
		names = append(names, fmt.Sprintf("ns-%03d", at))
	}
	mgr := adviceManager(t, boundTo(), names...)

	_, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "", 0, nil)

	if !errors.Is(err, ErrNamespaceNeeded) {
		t.Fatalf("error = %v, want the advice kept when some namespaces went unchecked", err)
	}
}

func TestALongListOfReadableNamespacesIsShortened(t *testing.T) {
	got := fewOf([]string{"a", "b", "c", "d", "e", "f", "g"})

	if got != "a, b, c, d, e and 2 more" {
		t.Fatalf("names = %q, want the first five and a count", got)
	}
}