// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 package meta_test import ( "testing" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/projectcapsule/capsule/pkg/api/meta" "github.com/projectcapsule/capsule/pkg/runtime/gvk" ) func mkItem(tenant, namespace, name, kind string, status metav1.ConditionStatus, condType, msg string, created bool, lastApply metav1.Time) meta.ObjectReferenceStatus { return meta.ObjectReferenceStatus{ ResourceID: gvk.ResourceID{ TenantResourceIDWithOrigin: gvk.TenantResourceIDWithOrigin{ TenantResourceID: gvk.TenantResourceID{Tenant: tenant}, Origin: "", }, Group: "", Version: "", Kind: kind, Name: name, Namespace: namespace, }, ObjectReferenceStatusCondition: meta.ObjectReferenceStatusCondition{ Status: status, Type: condType, Message: msg, LastApply: lastApply, Created: created, }, } } func TestProcessedItems_UpdateItem_AppendsNew(t *testing.T) { var p meta.ProcessedItems now := metav1.NewTime(time.Now()) item := mkItem("t1", "ns1", "name1", "Secret", metav1.ConditionTrue, "Ready", "ok", true, now) p.UpdateItem(item) if len(p) != 1 { t.Fatalf("expected 1 item, got %d", len(p)) } if p[0].ResourceID != item.ResourceID { t.Fatalf("expected ResourceID %+v, got %+v", item.ResourceID, p[0].ResourceID) } if p[0].ObjectReferenceStatusCondition != item.ObjectReferenceStatusCondition { t.Fatalf("expected condition %+v, got %+v", item.ObjectReferenceStatusCondition, p[0].ObjectReferenceStatusCondition) } } func TestProcessedItems_UpdateItem_UpdatesExistingWithoutDuplicate(t *testing.T) { now1 := metav1.NewTime(time.Now().Add(-1 * time.Hour)) now2 := metav1.NewTime(time.Now()) original := mkItem("t1", "ns1", "name1", "Secret", metav1.ConditionFalse, "Ready", "initial", false, now1) updated := mkItem("t1", "ns1", "name1", "Secret", metav1.ConditionTrue, "Ready", "updated", true, now2) p := meta.ProcessedItems{original} p.UpdateItem(updated) if len(p) != 1 { t.Fatalf("expected 1 item (updated in place), got %d", len(p)) } // Resource identity must remain same if p[0].ResourceID != original.ResourceID { t.Fatalf("expected ResourceID unchanged %+v, got %+v", original.ResourceID, p[0].ResourceID) } // Condition should be replaced if p[0].ObjectReferenceStatusCondition != updated.ObjectReferenceStatusCondition { t.Fatalf("expected condition replaced with %+v, got %+v", updated.ObjectReferenceStatusCondition, p[0].ObjectReferenceStatusCondition) } } func TestProcessedItems_RemoveItem_RemovesMatchingByResourceID(t *testing.T) { now := metav1.NewTime(time.Now()) a := mkItem("t1", "ns1", "name1", "Secret", metav1.ConditionTrue, "Ready", "a", true, now) b := mkItem("t1", "ns1", "name2", "ConfigMap", metav1.ConditionTrue, "Ready", "b", true, now) c := mkItem("t2", "ns9", "name9", "Secret", metav1.ConditionFalse, "Ready", "c", false, now) p := meta.ProcessedItems{a, b, c} // Remove b p.RemoveItem(b) if len(p) != 2 { t.Fatalf("expected 2 items after removal, got %d", len(p)) } // Ensure b is gone, others remain for _, it := range p { if it.ResourceID == b.ResourceID { t.Fatalf("did not expect removed item %+v to remain", b.ResourceID) } } // Ensure a and c still exist foundA, foundC := false, false for _, it := range p { if it.ResourceID == a.ResourceID { foundA = true } if it.ResourceID == c.ResourceID { foundC = true } } if !foundA || !foundC { t.Fatalf("expected remaining items to include a=%v c=%v", foundA, foundC) } } func TestProcessedItems_GetItem_ReturnsPointerToSliceElement(t *testing.T) { now := metav1.NewTime(time.Now()) a := mkItem("t1", "ns1", "name1", "Secret", metav1.ConditionFalse, "Ready", "a", false, now) b := mkItem("t1", "ns1", "name2", "Secret", metav1.ConditionFalse, "Ready", "b", false, now) p := meta.ProcessedItems{a, b} ptr := p.GetItem(b.ResourceID) if ptr == nil { t.Fatalf("expected to find item %v", b.ResourceID) } // Mutate through pointer and ensure slice reflects it (proves it's not a copy) ptr.Message = "changed" ptr.Status = metav1.ConditionTrue ptr.Created = true if p[1].Message != "changed" { t.Fatalf("expected slice element Message to be changed, got %q", p[1].Message) } if p[1].Status != metav1.ConditionTrue { t.Fatalf("expected slice element Status to be changed, got %q", p[1].Status) } if p[1].Created != true { t.Fatalf("expected slice element Created to be changed, got %v", p[1].Created) } // Not found case none := p.GetItem(gvk.ResourceID{Name: "does-not-exist"}) if none != nil { t.Fatalf("expected nil for non-existent item, got %+v", none) } } func TestProcessedItems_SortDeterministic(t *testing.T) { now := metav1.NewTime(time.Now()) // Intentionally shuffled order i1 := mkItem("tenant-b", "ns-a", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now) i2 := mkItem("tenant-a", "ns-b", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now) i3 := mkItem("tenant-a", "ns-a", "name-b", "Secret", metav1.ConditionTrue, "Ready", "", true, now) i4 := mkItem("tenant-a", "ns-a", "name-a", "ZKind", metav1.ConditionTrue, "Ready", "", true, now) i5 := mkItem("tenant-a", "ns-a", "name-a", "AKind", metav1.ConditionTrue, "Ready", "", true, now) i6 := mkItem("tenant-b", "ns-a", "name-a", "ConfigMap", metav1.ConditionTrue, "Ready", "", true, now) p := meta.ProcessedItems{i1, i2, i3, i4, i5, i6} p.SortDeterministic() // Expected order by: Tenant, Namespace, Name, Kind want := []gvk.ResourceID{ i5.ResourceID, // tenant-a, ns-a, name-a, AKind i4.ResourceID, // tenant-a, ns-a, name-a, ZKind i3.ResourceID, // tenant-a, ns-a, name-b, Secret i2.ResourceID, // tenant-a, ns-b, name-a, Secret i6.ResourceID, // tenant-b, ns-a, name-a, ConfigMap i1.ResourceID, // tenant-b, ns-a, name-a, Secret } if len(p) != len(want) { t.Fatalf("expected %d items, got %d", len(want), len(p)) } for idx := range want { if p[idx].ResourceID != want[idx] { t.Fatalf("at index %d: expected %v, got %v", idx, want[idx], p[idx].ResourceID) } } } func TestProcessedItems_InScope(t *testing.T) { now := metav1.NewTime(time.Now()) inA := mkItem("tenant-a", "ns-a", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now) inB := mkItem("tenant-a", "ns-a", "name-b", "ConfigMap", metav1.ConditionTrue, "Ready", "", true, now) otherNs := mkItem("tenant-a", "ns-b", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now) otherTnt := mkItem("tenant-b", "ns-a", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now) clusterWide := mkItem("tenant-a", "", "name-a", "ClusterRole", metav1.ConditionTrue, "Ready", "", true, now) p := meta.ProcessedItems{inA, otherNs, inB, otherTnt, clusterWide} scoped := p.InScope("tenant-a", "ns-a") if len(scoped) != 2 { t.Fatalf("expected 2 scoped items, got %d", len(scoped)) } if scoped[0].ResourceID != inA.ResourceID || scoped[1].ResourceID != inB.ResourceID { t.Fatalf("expected the scoped items in their original order, got %+v", scoped) } // The receiver must not be affected by any mutation of the returned copy. updated := inA updated.Message = "mutated" scoped.UpdateItem(updated) if p[0].Message != "" { t.Fatalf("expected the source item to be untouched, got message %q", p[0].Message) } } func TestProcessedItems_ReplaceScope(t *testing.T) { now := metav1.NewTime(time.Now()) tracked := mkItem("tenant-a", "ns-a", "name-a", "Secret", metav1.ConditionFalse, "Ready", "failed", true, now) gone := mkItem("tenant-a", "ns-a", "name-gone", "Secret", metav1.ConditionTrue, "Ready", "", true, now) otherNs := mkItem("tenant-a", "ns-b", "name-a", "Secret", metav1.ConditionTrue, "Ready", "keep", true, now) otherTnt := mkItem("tenant-b", "ns-a", "name-a", "Secret", metav1.ConditionTrue, "Ready", "keep", true, now) p := meta.ProcessedItems{tracked, otherNs, gone, otherTnt} reapplied := mkItem("tenant-a", "ns-a", "name-a", "Secret", metav1.ConditionTrue, "Ready", "applied", true, now) fresh := mkItem("tenant-a", "ns-a", "name-new", "ConfigMap", metav1.ConditionTrue, "Ready", "", false, now) // Out of the replaced scope: it must not leak into the result. foreign := mkItem("tenant-a", "ns-c", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now) p.ReplaceScope("tenant-a", "ns-a", meta.ProcessedItems{reapplied, fresh, foreign}) want := []gvk.ResourceID{ reapplied.ResourceID, // updated in place, position preserved otherNs.ResourceID, // untouched otherTnt.ResourceID, // untouched, shifted by the dropped item fresh.ResourceID, // appended } if len(p) != len(want) { t.Fatalf("expected %d items, got %d: %+v", len(want), len(p), p) } for idx := range want { if p[idx].ResourceID != want[idx] { t.Fatalf("at index %d: expected %v, got %v", idx, want[idx], p[idx].ResourceID) } } if p[0].Status != metav1.ConditionTrue || p[0].Message != "applied" { t.Fatalf("expected the tracked item condition to be replaced, got %+v", p[0].ObjectReferenceStatusCondition) } if p[1].Message != "keep" || p[2].Message != "keep" { t.Fatal("expected the items of the other scopes to keep their condition") } } func TestProcessedItems_ReplaceScope_EmptyDropsScopeOnly(t *testing.T) { now := metav1.NewTime(time.Now()) inScope := mkItem("tenant-a", "ns-a", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now) otherNs := mkItem("tenant-a", "ns-b", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now) p := meta.ProcessedItems{inScope, otherNs} p.ReplaceScope("tenant-a", "ns-a", nil) if len(p) != 1 || p[0].ResourceID != otherNs.ResourceID { t.Fatalf("expected only the out of scope item to survive, got %+v", p) } }