Ctrl K
ComunidadGuías…

internal/resources/search_test.go

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

Archivo original ↗Preguntar a KiBot
package resources

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

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	metadatafake "k8s.io/client-go/metadata/fake"
	k8stesting "k8s.io/client-go/testing"

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

func searchScheme() *runtime.Scheme {
	scheme := runtime.NewScheme()
	err := metav1.AddMetaToScheme(scheme)
	if err != nil {
		panic(err)
	}
	return scheme
}

func TestSearchReplacesNonpositiveLimitsWithSafeDefaults(t *testing.T) {
	got := searchLimits(CountLimits{Budget: -1, PerType: -1, Concurrency: -1})

	if got.Budget != searchTimeout {
		t.Fatalf("budget = %s, want %s", got.Budget, searchTimeout)
	}
	if got.PerType != searchPerType {
		t.Fatalf("per-type budget = %s, want %s", got.PerType, searchPerType)
	}
	if got.Concurrency != searchConcurrency {
		t.Fatalf("concurrency = %d, want %d", got.Concurrency, searchConcurrency)
	}
}

func meta(group, version, kind, namespace, name string) *metav1.PartialObjectMetadata {
	return &metav1.PartialObjectMetadata{
		APIVersion: schema.GroupVersion{Group: group, Version: version}.String(),
		Kind:       kind,
		Namespace:  namespace, Name: name,
	}
}

func descriptorsFor(keys ...string) []api.ResourceDescriptor {
	known := map[string]api.ResourceDescriptor{
		"/v1/pods": {Version: "v1", Resource: "pods", Kind: "Pod", Namespaced: true},
		"/v1/services": {
			Version: "v1", Resource: "services", Kind: "Service", Namespaced: true,
		},
		"/v1/configmaps": {
			Version: "v1", Resource: "configmaps", Kind: "ConfigMap", Namespaced: true,
		},
		"/v1/secrets": {
			Version: "v1", Resource: "secrets", Kind: "Secret", Namespaced: true,
		},
		"/v1/persistentvolumeclaims": {
			Version: "v1", Resource: "persistentvolumeclaims", Kind: "PersistentVolumeClaim", Namespaced: true,
		},
		"apps/v1/deployments": {
			Group: "apps", Version: "v1", Resource: "deployments", Kind: "Deployment", Namespaced: true,
		},
		"apps/v1/replicasets": {
			Group: "apps", Version: "v1", Resource: "replicasets", Kind: "ReplicaSet", Namespaced: true,
		},
	}
	out := make([]api.ResourceDescriptor, 0, len(keys))
	for _, key := range keys {
		out = append(out, known[key])
	}
	return out
}

func fakeMeta(t *testing.T, objects ...runtime.Object) *metadatafake.FakeMetadataClient {
	t.Helper()
	return metadatafake.NewSimpleMetadataClient(searchScheme(), objects...)
}

func names(results api.SearchResults) []string {
	out := make([]string, 0, len(results.Hits))
	for _, hit := range results.Hits {
		out = append(out, hit.Name)
	}
	return out
}

func TestSearchFindsObjectsByPartOfTheirName(t *testing.T) {
	client := fakeMeta(
		t,
		meta("", "v1", "Pod", "airbyte", "airbyte-server-0"),
		meta("", "v1", "Pod", "shop", "web-0"),
	)

	found := Search(context.Background(), client, descriptorsFor("/v1/pods"), "airbyte", CountLimits{})

	if len(found.Hits) != 1 {
		t.Fatalf("hits = %v, want the airbyte pod only", names(found))
	}
	hit := found.Hits[0]
	if hit.Kind != "Pod" || hit.Namespace != "airbyte" || hit.Resource != "pods" {
		t.Fatalf("hit = %+v", hit)
	}
}

func TestSearchReportsAPanickingResourceReader(t *testing.T) {
	client := fakeMeta(t)
	client.PrependReactor("list", "pods", func(k8stesting.Action) (bool, runtime.Object, error) {
		panic("broken metadata client")
	})

	found := Search(context.Background(), client, descriptorsFor("/v1/pods"), "airbyte", CountLimits{})

	if found.Errors["/v1/pods"] == "" {
		t.Fatalf("errors = %+v, want the panicking reader named", found.Errors)
	}
}

func TestSearchIgnoresCase(t *testing.T) {
	client := fakeMeta(t, meta("", "v1", "Pod", "shop", "Airbyte-Worker"))

	found := Search(context.Background(), client, descriptorsFor("/v1/pods"), "AIRBYTE", CountLimits{})

	if len(found.Hits) != 1 {
		t.Fatalf("a differently cased name was missed: %v", names(found))
	}
}

func TestSearchLooksAcrossSeveralKinds(t *testing.T) {
	client := fakeMeta(
		t,
		meta("", "v1", "Pod", "airbyte", "airbyte-server-0"),
		meta("apps", "v1", "Deployment", "airbyte", "airbyte-server"),
		meta("", "v1", "Service", "airbyte", "airbyte-api"),
	)

	found := Search(
		context.Background(),
		client,
		descriptorsFor("/v1/pods", "apps/v1/deployments", "/v1/services"),
		"airbyte",
		CountLimits{},
	)

	if len(found.Hits) != 3 {
		t.Fatalf("hits = %v, want all three kinds", names(found))
	}
}

func TestSearchLeavesOutKindsItDoesNotSweep(t *testing.T) {
	client := fakeMeta(t, meta("apps", "v1", "ReplicaSet", "airbyte", "airbyte-server-abc"))

	found := Search(
		context.Background(),
		client,
		descriptorsFor("apps/v1/replicasets"),
		"airbyte",
		CountLimits{},
	)

	if len(found.Hits) != 0 {
		t.Fatalf("hits = %v, want none from a kind outside the sweep", names(found))
	}
}

func TestSearchNeedsMoreThanASingleLetter(t *testing.T) {
	client := fakeMeta(t, meta("", "v1", "Pod", "shop", "a-pod"))

	found := Search(context.Background(), client, descriptorsFor("/v1/pods"), "a", CountLimits{})

	if len(found.Hits) != 0 {
		t.Fatalf("hits = %v, want nothing for a one-letter query", names(found))
	}
}

func TestSearchIgnoresSurroundingSpace(t *testing.T) {
	client := fakeMeta(t, meta("", "v1", "Pod", "shop", "airbyte-0"))

	found := Search(context.Background(), client, descriptorsFor("/v1/pods"), "  airbyte  ", CountLimits{})

	if len(found.Hits) != 1 {
		t.Fatalf("hits = %v, want the pod", names(found))
	}
}

func TestSearchSortsSoTheListIsStable(t *testing.T) {
	client := fakeMeta(
		t,
		meta("", "v1", "Pod", "shop", "airbyte-2"),
		meta("", "v1", "Pod", "apps", "airbyte-1"),
		meta("apps", "v1", "Deployment", "shop", "airbyte-dep"),
	)

	found := Search(
		context.Background(),
		client,
		descriptorsFor("/v1/pods", "apps/v1/deployments"),
		"airbyte",
		CountLimits{},
	)

	if names(found)[0] != "airbyte-dep" {
		t.Fatalf("order = %v, want Deployment before Pod", names(found))
	}
	if names(found)[1] != "airbyte-1" {
		t.Fatalf("order = %v, want namespaces in order", names(found))
	}
}

func TestSearchStopsAfterEnoughOfOneKind(t *testing.T) {
	objects := make([]runtime.Object, 0, searchPerKind+5)
	for index := range searchPerKind + 5 {
		objects = append(objects, meta("", "v1", "Pod", "shop", "airbyte-"+string(rune('a'+index))))
	}
	client := fakeMeta(t, objects...)

	found := Search(context.Background(), client, descriptorsFor("/v1/pods"), "airbyte", CountLimits{})

	if len(found.Hits) != searchPerKind {
		t.Fatalf("hits = %d, want the per-kind cap", len(found.Hits))
	}
	if !found.Truncated {
		t.Fatal("the caller was not told the list was cut short")
	}
}

func TestSearchStopsAfterEnoughResultsAcrossKinds(t *testing.T) {
	objects := make([]runtime.Object, 0, 6*searchPerKind)
	types := []struct {
		group    string
		kind     string
		key      string
		resource string
	}{
		{kind: "Pod", key: "/v1/pods", resource: "pods"},
		{kind: "Service", key: "/v1/services", resource: "services"},
		{kind: "ConfigMap", key: "/v1/configmaps", resource: "configmaps"},
		{kind: "Secret", key: "/v1/secrets", resource: "secrets"},
		{kind: "PersistentVolumeClaim", key: "/v1/persistentvolumeclaims", resource: "persistentvolumeclaims"},
		{group: "apps", kind: "Deployment", key: "apps/v1/deployments", resource: "deployments"},
	}
	keys := make([]string, 0, len(types))
	for _, resourceType := range types {
		keys = append(keys, resourceType.key)
		for index := range searchPerKind {
			name := fmt.Sprintf("airbyte-%s-%02d", resourceType.resource, index)
			objects = append(objects, meta(resourceType.group, "v1", resourceType.kind, "shop", name))
		}
	}
	client := fakeMeta(t, objects...)

	found := Search(context.Background(), client, descriptorsFor(keys...), "airbyte", CountLimits{})

	if len(found.Hits) != searchTotal {
		t.Fatalf("hits = %d, want the global cap", len(found.Hits))
	}
	if !found.Truncated {
		t.Fatal("the caller was not told the combined result was cut short")
	}
	for _, hit := range found.Hits {
		if hit.Kind == "Service" {
			t.Fatalf("sorting and truncation kept a service beyond the first %d hits", searchTotal)
		}
	}
}

func TestSearchSaysWhyAKindCouldNotBeRead(t *testing.T) {
	client := fakeMeta(t)
	client.PrependReactor("list", "pods", func(k8stesting.Action) (bool, runtime.Object, error) {
		return true, nil, errors.New("pods is forbidden")
	})

	found := Search(context.Background(), client, descriptorsFor("/v1/pods"), "airbyte", CountLimits{})

	if found.Errors["/v1/pods"] == "" {
		t.Fatalf("errors = %v, want the refusal", found.Errors)
	}
	if len(found.Hits) != 0 {
		t.Fatalf("hits = %v, want none", names(found))
	}
}

func TestNamespacedSearchErrorsNameTheNamespace(t *testing.T) {
	client := fakeMeta(t)
	client.PrependReactor("list", "pods", func(k8stesting.Action) (bool, runtime.Object, error) {
		return true, nil, errors.New("pods are forbidden in payments")
	})
	desc := descriptorsFor("/v1/pods")[0]

	_, _, failures := searchResource(
		t.Context(),
		client,
		desc,
		"airbyte",
		searchLimits(CountLimits{}),
		[]string{"payments"},
	)

	if failures["/v1/pods/payments"] == nil {
		t.Fatalf("failures = %v, want the refused namespace distinguished", failures)
	}
}

func TestSearchCarriesOnWhenOneKindFails(t *testing.T) {
	client := fakeMeta(t, meta("apps", "v1", "Deployment", "airbyte", "airbyte-server"))
	client.PrependReactor("list", "pods", func(k8stesting.Action) (bool, runtime.Object, error) {
		return true, nil, errors.New("pods is forbidden")
	})

	found := Search(
		context.Background(),
		client,
		descriptorsFor("/v1/pods", "apps/v1/deployments"),
		"airbyte",
		CountLimits{},
	)

	if len(found.Hits) != 1 {
		t.Fatalf("hits = %v, want the deployment", names(found))
	}
}

func TestSearchHasItsOwnLimits(t *testing.T) {
	limits := searchLimits(CountLimits{})

	if limits.Budget != searchTimeout || limits.PerType != searchPerType {
		t.Fatalf("limits = %+v", limits)
	}
	if limits.Concurrency != searchConcurrency {
		t.Fatalf("concurrency = %d", limits.Concurrency)
	}
}

func TestSearchKeepsTheLimitsItWasGiven(t *testing.T) {
	limits := searchLimits(CountLimits{Budget: time.Second, PerType: time.Second, Concurrency: 2})

	if limits.Budget != time.Second || limits.Concurrency != 2 {
		t.Fatalf("limits = %+v", limits)
	}
}

func TestAManagerWithoutAMetadataClientFindsNothing(t *testing.T) {
	manager := &Manager{}

	found := manager.Search(context.Background(), "airbyte")

	if len(found.Hits) != 0 {
		t.Fatalf("hits = %v, want none", names(found))
	}
}

func TestAManagerSearchesTheKindsItKnows(t *testing.T) {
	client := fakeMeta(t, meta("", "v1", "Pod", "airbyte", "airbyte-server-0"))
	manager := &Manager{
		meta:  client,
		descs: map[string]api.ResourceDescriptor{"/v1/pods": descriptorsFor("/v1/pods")[0]},
	}

	found := manager.Search(context.Background(), "airbyte")

	if len(found.Hits) != 1 {
		t.Fatalf("hits = %v, want the pod", names(found))
	}
}

func namespacesManager(t *testing.T, objects ...runtime.Object) *Manager {
	t.Helper()
	return NewManager(t.Context(), Deps{Metadata: fakeMeta(t, objects...)})
}

func TestNamespacesComeBackSorted(t *testing.T) {
	mgr := namespacesManager(
		t,
		meta("", "v1", "Namespace", "", "shop"),
		meta("", "v1", "Namespace", "", "argocd"),
		meta("", "v1", "Namespace", "", "kube-system"),
	)

	got := mgr.Namespaces(context.Background())

	want := []string{"argocd", "kube-system", "shop"}
	if len(got.Names) != len(want) {
		t.Fatalf("names = %v, want %v", got.Names, want)
	}
	for i, name := range want {
		if got.Names[i] != name {
			t.Fatalf("names = %v, want %v", got.Names, want)
		}
	}
	if got.Error != "" {
		t.Fatalf("error = %q, want none", got.Error)
	}
}

func TestNamespacesCarryTheRefusalInstead(t *testing.T) {
	mgr := namespacesManager(t)
	client, ok := mgr.meta.(*metadatafake.FakeMetadataClient)
	if !ok {
		t.Fatal("the manager is not holding the fake metadata client")
	}
	client.PrependReactor("list", "namespaces", func(k8stesting.Action) (bool, runtime.Object, error) {
		return true, nil, errors.New("namespaces is forbidden")
	})

	got := mgr.Namespaces(context.Background())

	if got.Error == "" {
		t.Fatal("a refused listing came back as an empty cluster")
	}
	if len(got.Names) != 0 {
		t.Fatalf("names = %v, want none", got.Names)
	}
}

func TestNamespacesWithoutAClusterAreEmpty(t *testing.T) {
	mgr := NewManager(t.Context(), Deps{})

	got := mgr.Namespaces(context.Background())

	if len(got.Names) != 0 || got.Error != "" {
		t.Fatalf("namespaces = %+v, want nothing without a cluster", got)
	}
}

func TestNamespacesAreLimitedToWhatTheAccountCanRead(t *testing.T) {
	manager := NewManager(t.Context(), Deps{
		Metadata: namespacesNamed("payments", "kube-system", "storefront"),
		Perms:    boundTo("payments", "storefront"),
	})

	found := manager.Namespaces(alice(t))

	want := []string{"payments", "storefront"}
	if len(found.Names) != len(want) {
		t.Fatalf("names = %v, want %v", found.Names, want)
	}
	for index, name := range want {
		if found.Names[index] != name {
			t.Fatalf("names = %v, want %v", found.Names, want)
		}
	}
}