Ctrl K
ComunidadGuías…

internal/inspect/events_test.go

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

Archivo original ↗Preguntar a KiBot
package inspect

import (
	"context"
	"errors"
	"maps"
	"slices"
	"strings"
	"testing"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	k8stesting "k8s.io/client-go/testing"

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

func newEvent(name string, fields map[string]any) *unstructured.Unstructured {
	obj := map[string]any{
		"apiVersion": "v1",
		"kind":       "Event",
		"metadata": map[string]any{
			"name":      name,
			"namespace": "flux-system",
		},
		"involvedObject": map[string]any{"uid": "6f1c0d3e-4a2b-4c8d-9e10-2b7f5a6c1d84"},
	}
	maps.Copy(obj, fields)
	return &unstructured.Unstructured{Object: obj}
}

func TestEventsMapsAndSortsNewestFirst(t *testing.T) {
	older := newEvent("older", map[string]any{
		"type":           "Normal",
		"reason":         "Pulled",
		"message":        "image pulled",
		"count":          int64(2),
		"firstTimestamp": "2026-07-27T08:00:00Z",
		"lastTimestamp":  "2026-07-27T08:30:00Z",
		"source":         map[string]any{"component": "kubelet"},
	})
	newer := newEvent("newer", map[string]any{
		"type":           "Warning",
		"reason":         "BackOff",
		"message":        "restarting",
		"count":          int64(7),
		"firstTimestamp": "2026-07-27T09:00:00Z",
		"lastTimestamp":  "2026-07-27T09:30:00Z",
		"source":         map[string]any{"component": "kubelet"},
	})

	events, _ := Events(context.Background(), newClient(older, newer), "flux-system", "6f1c0d3e-4a2b-4c8d-9e10-2b7f5a6c1d84")

	if len(events) != 2 {
		t.Fatalf("events = %d, want 2", len(events))
	}
	if events[0].Reason != "BackOff" {
		t.Fatalf("first reason = %q, want BackOff (newest first)", events[0].Reason)
	}
	if events[0].Type != "Warning" {
		t.Fatalf("type = %q, want Warning", events[0].Type)
	}
	if events[0].Message != "restarting" {
		t.Fatalf("message = %q", events[0].Message)
	}
	if events[0].Source != "kubelet" {
		t.Fatalf("source = %q, want kubelet", events[0].Source)
	}
	if events[0].Count != 7 {
		t.Fatalf("count = %d, want 7", events[0].Count)
	}
	if events[0].FirstSeen != "2026-07-27T09:00:00Z" {
		t.Fatalf("firstSeen = %q", events[0].FirstSeen)
	}
	if events[0].LastSeen != "2026-07-27T09:30:00Z" {
		t.Fatalf("lastSeen = %q", events[0].LastSeen)
	}
}

func TestEventsSendsUIDFieldSelector(t *testing.T) {
	client := newClient()
	selector := ""
	client.PrependReactor("list", "events", func(action k8stesting.Action) (bool, runtime.Object, error) {
		list, ok := action.(k8stesting.ListAction)
		if ok {
			selector = list.GetListRestrictions().Fields.String()
		}
		return false, nil, nil
	})

	_, _ = Events(context.Background(), client, "flux-system", "6f1c0d3e-4a2b-4c8d-9e10-2b7f5a6c1d84")

	if selector != "involvedObject.uid=6f1c0d3e-4a2b-4c8d-9e10-2b7f5a6c1d84" {
		t.Fatalf("field selector = %q, want involvedObject.uid=6f1c0d3e-4a2b-4c8d-9e10-2b7f5a6c1d84", selector)
	}
}

func TestEventsWithoutNamespaceListsClusterWide(t *testing.T) {
	client := newClient()
	namespace := "unset"
	client.PrependReactor("list", "events", func(action k8stesting.Action) (bool, runtime.Object, error) {
		namespace = action.GetNamespace()
		return false, nil, nil
	})

	_, _ = Events(context.Background(), client, "", "1c3a5e70-9b8d-4f21-a3c6-7e5d4b2a1908")

	if namespace != "" {
		t.Fatalf("namespace = %q, want empty for a cluster-wide list", namespace)
	}
}

func TestEventsRefusesAUIDThatIsNotOne(t *testing.T) {
	cases := map[string]string{
		"an injected second clause": "6f1c0d3e-4a2b-4c8d-9e10-2b7f5a6c1d84,involvedObject.namespace=kube-system",
		"a name rather than a uid":  "web-59d8f",
		"a truncated uid":           "6f1c0d3e-4a2b-4c8d-9e10",
		"trailing whitespace":       "6f1c0d3e-4a2b-4c8d-9e10-2b7f5a6c1d84 ",
	}
	for name, uid := range cases {
		t.Run(name, func(t *testing.T) {
			listed := false
			client := newClient()
			client.PrependReactor("list", "events", func(k8stesting.Action) (bool, runtime.Object, error) {
				listed = true
				return false, nil, nil
			})

			_, err := Events(context.Background(), client, "flux-system", uid)

			if !errors.Is(err, ErrInvalidUID) {
				t.Fatalf("err = %v, want it refused before the selector is built", err)
			}
			if listed {
				t.Fatal("the apiserver was asked with an unvalidated field selector")
			}
		})
	}
}

func TestEventsEmptyWithoutUID(t *testing.T) {
	events, _ := Events(context.Background(), newClient(), "flux-system", "")
	if len(events) != 0 {
		t.Fatalf("events = %d, want 0", len(events))
	}
}

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

	events, err := Events(context.Background(), client, "flux-system", "6f1c0d3e-4a2b-4c8d-9e10-2b7f5a6c1d84")

	if err == nil {
		t.Fatal("a list failure was reported as an empty event list")
	}
	if !strings.Contains(err.Error(), "events is forbidden") {
		t.Fatalf("err = %v, want the reason from the apiserver", err)
	}
	if events != nil {
		t.Fatalf("events = %v, want nil alongside the error", events)
	}
}

func TestEventCountFallsBackToSeries(t *testing.T) {
	event := newEvent("series", map[string]any{
		"series":    map[string]any{"count": int64(4), "lastObservedTime": "2026-07-27T11:00:00Z"},
		"eventTime": "2026-07-27T10:00:00Z",
	})

	events, _ := Events(context.Background(), newClient(event), "flux-system", "6f1c0d3e-4a2b-4c8d-9e10-2b7f5a6c1d84")

	if len(events) != 1 {
		t.Fatalf("events = %d, want 1", len(events))
	}
	if events[0].Count != 4 {
		t.Fatalf("count = %d, want 4", events[0].Count)
	}
	if events[0].LastSeen != "2026-07-27T11:00:00Z" {
		t.Fatalf("lastSeen = %q, want the series observation time", events[0].LastSeen)
	}
}

func TestEventCountDefaultsToOne(t *testing.T) {
	event := newEvent("bare", map[string]any{
		"eventTime":          "2026-07-27T10:00:00Z",
		"reportingComponent": "kustomize-controller",
	})

	events, _ := Events(context.Background(), newClient(event), "flux-system", "6f1c0d3e-4a2b-4c8d-9e10-2b7f5a6c1d84")

	if len(events) != 1 {
		t.Fatalf("events = %d, want 1", len(events))
	}
	if events[0].Count != 1 {
		t.Fatalf("count = %d, want 1", events[0].Count)
	}
	if events[0].Source != "kustomize-controller" {
		t.Fatalf("source = %q, want the reporting component", events[0].Source)
	}
	if events[0].LastSeen != "2026-07-27T10:00:00Z" {
		t.Fatalf("lastSeen = %q, want the event time", events[0].LastSeen)
	}
}

func TestEventLastSeenEmptyWhenNoTimestamps(t *testing.T) {
	events, _ := Events(context.Background(), newClient(newEvent("bare", nil)), "flux-system", "6f1c0d3e-4a2b-4c8d-9e10-2b7f5a6c1d84")

	if len(events) != 1 {
		t.Fatalf("events = %d, want 1", len(events))
	}
	if events[0].LastSeen != "" {
		t.Fatalf("lastSeen = %q, want empty", events[0].LastSeen)
	}
}

func TestEventsSortMixesTimestampPrecision(t *testing.T) {
	secondPrecision := newEvent("second", map[string]any{
		"reason":        "Pulled",
		"lastTimestamp": "2026-07-27T09:34:00Z",
	})
	subSecond := newEvent("sub-second", map[string]any{
		"reason":    "Scheduled",
		"eventTime": "2026-07-27T09:34:00.546384Z",
	})

	events, _ := Events(context.Background(), newClient(secondPrecision, subSecond), "flux-system", "6f1c0d3e-4a2b-4c8d-9e10-2b7f5a6c1d84")

	if len(events) != 2 {
		t.Fatalf("events = %d, want 2", len(events))
	}
	if events[0].Reason != "Scheduled" {
		t.Fatalf("first reason = %q, want Scheduled (the later sub-second stamp)", events[0].Reason)
	}
}

func TestEventsSortKeepsUnparseableStampsLast(t *testing.T) {
	good := newEvent("good", map[string]any{
		"reason":        "Pulled",
		"lastTimestamp": "2026-07-27T09:34:00Z",
	})
	bad := newEvent("bad", map[string]any{"reason": "Broken"})

	events, _ := Events(context.Background(), newClient(good, bad), "flux-system", "6f1c0d3e-4a2b-4c8d-9e10-2b7f5a6c1d84")

	if len(events) != 2 {
		t.Fatalf("events = %d, want 2", len(events))
	}
	if events[0].Reason != "Pulled" {
		t.Fatalf("first reason = %q, want Pulled", events[0].Reason)
	}
}

func TestEventObjectReadsEitherApiShape(t *testing.T) {
	cases := []struct {
		name  string
		event map[string]any
		want  string
	}{
		{
			name:  "the core involvedObject",
			event: map[string]any{"involvedObject": map[string]any{"kind": "Pod", "name": "web-0", "namespace": "prod"}},
			want:  "Pod prod/web-0",
		},
		{
			name:  "the events.k8s.io regarding",
			event: map[string]any{"regarding": map[string]any{"kind": "Pod", "name": "web-0", "namespace": "prod"}},
			want:  "Pod prod/web-0",
		},
		{
			name:  "a cluster-scoped object",
			event: map[string]any{"involvedObject": map[string]any{"kind": "Node", "name": "node-1"}},
			want:  "Node/node-1",
		},
		{
			name: "an empty involvedObject alongside a filled regarding",
			event: map[string]any{
				"involvedObject": map[string]any{},
				"regarding":      map[string]any{"kind": "Pod", "name": "web-0"},
			},
			want: "Pod/web-0",
		},
		{name: "neither", event: map[string]any{}, want: ""},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			item := &unstructured.Unstructured{Object: tc.event}

			if got := eventObjectOf(item); got != tc.want {
				t.Fatalf("object = %q, want %q", got, tc.want)
			}
		})
	}
}

func TestEventSourceJoinsWhatItKnows(t *testing.T) {
	cases := []struct {
		name  string
		event map[string]any
		want  string
	}{
		{
			name:  "component and host",
			event: map[string]any{"source": map[string]any{"component": "kubelet", "host": "node-1"}},
			want:  "kubelet on node-1",
		},
		{
			name:  "component alone",
			event: map[string]any{"source": map[string]any{"component": "kubelet"}},
			want:  "kubelet",
		},
		{
			name:  "host alone",
			event: map[string]any{"source": map[string]any{"host": "node-1"}},
			want:  "node-1",
		},
		{
			name:  "the events.k8s.io reporting fields",
			event: map[string]any{"reportingComponent": "kustomize-controller", "reportingInstance": "flux-system"},
			want:  "kustomize-controller on flux-system",
		},
		{name: "nothing at all", event: map[string]any{}, want: ""},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			item := &unstructured.Unstructured{Object: tc.event}

			if got := eventSourceOf(item); got != tc.want {
				t.Fatalf("source = %q, want %q", got, tc.want)
			}
		})
	}
}

func TestEventCountReadsTheDeprecatedField(t *testing.T) {
	item := &unstructured.Unstructured{Object: map[string]any{"deprecatedCount": int64(7)}}

	if got := eventCountOf(item); got != 7 {
		t.Fatalf("count = %d, want 7", got)
	}
}

func TestFirstOfHasNothingToReturn(t *testing.T) {
	item := &unstructured.Unstructured{Object: map[string]any{}}

	if got := firstOf(item, []string{"missing"}, []string{"absent"}); got != "" {
		t.Fatalf("found = %q, want empty", got)
	}
}

func TestEventsAtTheSameInstantKeepOneOrder(t *testing.T) {
	same := "2026-08-29T12:00:00Z"
	build := func() []api.Event {
		return []api.Event{
			{Reason: "Pulled", Source: "kubelet", Message: "c", LastSeen: same},
			{Reason: "Created", Source: "kubelet", Message: "a", LastSeen: same},
			{Reason: "Started", Source: "kubelet", Message: "b", LastSeen: same},
			{Reason: "Killing", Source: "kubelet", Message: "d", LastSeen: same},
		}
	}

	first := build()
	sortEvents(first)

	for _, order := range [][]int{{3, 2, 1, 0}, {1, 3, 0, 2}, {2, 0, 3, 1}} {
		shuffled := build()
		mixed := make([]api.Event, 0, len(order))
		for _, at := range order {
			mixed = append(mixed, shuffled[at])
		}
		sortEvents(mixed)
		if !slices.Equal(reasons(mixed), reasons(first)) {
			t.Fatalf(
				"the same events arrived in a different order and came out %v, not %v; "+
					"a tie on lastSeen must not leave the order to whatever the map handed us",
				reasons(mixed), reasons(first),
			)
		}
	}
}

func reasons(events []api.Event) []string {
	out := make([]string, 0, len(events))
	for _, one := range events {
		out = append(out, one.Reason)
	}
	return out
}

func TestTheNewestEventStillComesFirst(t *testing.T) {
	events := []api.Event{
		{Reason: "Older", LastSeen: "2026-08-29T11:00:00Z"},
		{Reason: "Newer", LastSeen: "2026-08-29T12:00:00Z"},
	}

	sortEvents(events)

	if events[0].Reason != "Newer" {
		t.Fatalf("first = %q, want the newest event", events[0].Reason)
	}
}