internal/inspect/events_test.go
Rama main · réplica de lectura del repositorio original.
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)
}
}