Ctrl K
ComunidadGuías…

internal/flux/actions_test.go

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

Archivo original ↗Preguntar a KiBot
package flux

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

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic/fake"

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

var (
	kustomizationGVR = schema.GroupVersionResource{
		Group:    "kustomize.toolkit.fluxcd.io",
		Version:  "v1",
		Resource: "kustomizations",
	}
	clusterScopedGVR = schema.GroupVersionResource{
		Group:    "notification.toolkit.fluxcd.io",
		Version:  "v1beta3",
		Resource: "providers",
	}
)

var stamp = time.Date(2026, 7, 27, 16, 30, 0, 0, time.UTC)

func actionClient(objs ...runtime.Object) *fake.FakeDynamicClient {
	kinds := map[schema.GroupVersionResource]string{
		kustomizationGVR: "KustomizationList",
		clusterScopedGVR: "ProviderList",
	}
	return fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), kinds, objs...)
}

func newKustomization(suspended bool) *unstructured.Unstructured {
	return &unstructured.Unstructured{Object: map[string]any{
		"apiVersion": "kustomize.toolkit.fluxcd.io/v1",
		"kind":       "Kustomization",
		"metadata": map[string]any{
			"name":      "apps",
			"namespace": "flux-system",
		},
		"spec": map[string]any{
			"interval": "10m",
			"suspend":  suspended,
		},
	}}
}

func kustomizationRef() api.ObjectRef {
	return api.ObjectRef{
		Group:     "kustomize.toolkit.fluxcd.io",
		Version:   "v1",
		Resource:  "kustomizations",
		Namespace: "flux-system",
		Name:      "apps",
	}
}

func readBack(t *testing.T, client *fake.FakeDynamicClient) *unstructured.Unstructured {
	t.Helper()
	got, err := client.Resource(kustomizationGVR).
		Namespace("flux-system").
		Get(context.Background(), "apps", metav1.GetOptions{})
	if err != nil {
		t.Fatalf("read back: %v", err)
	}
	return got
}

func TestReconcileSetsTheRequestAnnotation(t *testing.T) {
	client := actionClient(newKustomization(false))

	_, err := Do(context.Background(), client, nil, kustomizationRef(), Reconcile, stamp)
	if err != nil {
		t.Fatalf("reconcile: %v", err)
	}
	got := readBack(t, client)
	want := "2026-07-27T16:30:00Z"
	if got.GetAnnotations()[reconcileAnnotation] != want {
		t.Fatalf("annotation = %q, want %q", got.GetAnnotations()[reconcileAnnotation], want)
	}
}

func TestReconcileLeavesTheSpecAlone(t *testing.T) {
	client := actionClient(newKustomization(false))

	_, err := Do(context.Background(), client, nil, kustomizationRef(), Reconcile, stamp)
	if err != nil {
		t.Fatalf("reconcile: %v", err)
	}

	got := readBack(t, client)
	interval, _, _ := unstructured.NestedString(got.Object, "spec", "interval")
	if interval != "10m" {
		t.Fatalf("spec.interval = %q, want 10m", interval)
	}
	suspended, _, _ := unstructured.NestedBool(got.Object, "spec", "suspend")
	if suspended {
		t.Fatalf("reconcile changed spec.suspend")
	}
}

func TestSuspendSetsTheField(t *testing.T) {
	client := actionClient(newKustomization(false))

	_, err := Do(context.Background(), client, nil, kustomizationRef(), Suspend, stamp)
	if err != nil {
		t.Fatalf("suspend: %v", err)
	}
	got := readBack(t, client)
	suspended, found, _ := unstructured.NestedBool(got.Object, "spec", "suspend")
	if !found || !suspended {
		t.Fatalf("spec.suspend = %v (found %v), want true", suspended, found)
	}
}

func TestResumeClearsTheField(t *testing.T) {
	client := actionClient(newKustomization(true))

	_, err := Do(context.Background(), client, nil, kustomizationRef(), Resume, stamp)
	if err != nil {
		t.Fatalf("resume: %v", err)
	}
	got := readBack(t, client)
	suspended, found, _ := unstructured.NestedBool(got.Object, "spec", "suspend")
	if !found || suspended {
		t.Fatalf("spec.suspend = %v (found %v), want false", suspended, found)
	}
}

func TestSuspendDoesNotAnnotate(t *testing.T) {
	client := actionClient(newKustomization(false))

	_, err := Do(context.Background(), client, nil, kustomizationRef(), Suspend, stamp)
	if err != nil {
		t.Fatalf("suspend: %v", err)
	}

	if readBack(t, client).GetAnnotations()[reconcileAnnotation] != "" {
		t.Fatalf("suspend added a reconcile annotation")
	}
}

func TestClusterScopedTarget(t *testing.T) {
	provider := &unstructured.Unstructured{Object: map[string]any{
		"apiVersion": "notification.toolkit.fluxcd.io/v1beta3",
		"kind":       "Provider",
		"metadata":   map[string]any{"name": "slack"},
	}}
	client := actionClient(provider)
	ref := api.ObjectRef{
		Group:    "notification.toolkit.fluxcd.io",
		Version:  "v1beta3",
		Resource: "providers",
		Name:     "slack",
	}

	_, err := Do(context.Background(), client, nil, ref, Suspend, stamp)
	if err != nil {
		t.Fatalf("suspend: %v", err)
	}
	got, getErr := client.Resource(clusterScopedGVR).Get(context.Background(), "slack", metav1.GetOptions{})
	if getErr != nil {
		t.Fatalf("read back: %v", getErr)
	}
	suspended, _, _ := unstructured.NestedBool(got.Object, "spec", "suspend")
	if !suspended {
		t.Fatalf("cluster-scoped suspend did not apply")
	}
}

func TestRejectsNonFluxGroup(t *testing.T) {
	client := actionClient()
	ref := api.ObjectRef{Group: "apps", Version: "v1", Resource: "deployments", Namespace: "d", Name: "web"}

	_, err := Do(context.Background(), client, nil, ref, Reconcile, stamp)

	if err == nil {
		t.Fatalf("expected a non-flux group to be rejected")
	}
}

func TestRejectsUnknownAction(t *testing.T) {
	client := actionClient(newKustomization(false))

	_, err := Do(context.Background(), client, nil, kustomizationRef(), Action("explode"), stamp)

	if err == nil {
		t.Fatalf("expected an unknown action to be rejected")
	}
}

func TestPropagatesAPIError(t *testing.T) {
	client := actionClient()

	_, err := Do(context.Background(), client, nil, kustomizationRef(), Reconcile, stamp)

	if err == nil {
		t.Fatalf("expected an error patching a missing object")
	}
}

func TestIsFluxGroup(t *testing.T) {
	cases := map[string]bool{
		"kustomize.toolkit.fluxcd.io":    true,
		"source.toolkit.fluxcd.io":       true,
		"helm.toolkit.fluxcd.io":         true,
		"notification.toolkit.fluxcd.io": true,
		"image.toolkit.fluxcd.io":        true,
		"apps":                           false,
		"":                               false,
		"fluxcd.io":                      false,
		"toolkit.fluxcd.io":              false,
	}
	for group, want := range cases {
		if got := IsFluxGroup(group); got != want {
			t.Fatalf("IsFluxGroup(%q) = %v, want %v", group, got, want)
		}
	}
}

func TestDescriptorLookupIgnoresOtherGroupsAndKinds(t *testing.T) {
	cases := []map[string]api.ResourceDescriptor{
		{
			"deployment": {Group: "apps", Version: "v1", Resource: "deployments", Kind: "Deployment"},
		},
		{
			"helm": {Group: "source.toolkit.fluxcd.io", Version: "v1", Resource: "helmrepositories", Kind: "HelmRepository"},
		},
	}
	for _, descs := range cases {
		if desc, found := descriptorOf(descs, "GitRepository"); found {
			t.Fatalf("descriptor = %+v, want no GitRepository", desc)
		}
	}
}

func sourceDescs() map[string]api.ResourceDescriptor {
	return map[string]api.ResourceDescriptor{
		"gitrepositories": {
			Group:      "source.toolkit.fluxcd.io",
			Version:    "v1",
			Resource:   "gitrepositories",
			Kind:       "GitRepository",
			Namespaced: true,
		},
	}
}

func newSource() *unstructured.Unstructured {
	return &unstructured.Unstructured{Object: map[string]any{
		"apiVersion": "source.toolkit.fluxcd.io/v1",
		"kind":       "GitRepository",
		"metadata":   map[string]any{"name": "flux-system", "namespace": "flux-system"},
		"spec":       map[string]any{"url": "https://example.test/infra"},
	}}
}

func sourcedKustomization() *unstructured.Unstructured {
	obj := newKustomization(false)
	_ = unstructured.SetNestedMap(obj.Object, map[string]any{
		"kind": "GitRepository",
		"name": "flux-system",
	}, "spec", "sourceRef")
	return obj
}

func sourceClient(t *testing.T, objs ...runtime.Object) *fake.FakeDynamicClient {
	t.Helper()
	kinds := map[schema.GroupVersionResource]string{
		kustomizationGVR: "KustomizationList",
		{Group: "source.toolkit.fluxcd.io", Version: "v1", Resource: "gitrepositories"}: "GitRepositoryList",
	}
	return fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), kinds, objs...)
}

func TestReconcileWithSourceAsksTheSourceFirst(t *testing.T) {
	client := sourceClient(t, sourcedKustomization(), newSource())

	result, err := Do(t.Context(), client, sourceDescs(), kustomizationRef(), ReconcileSource, stamp)
	if err != nil {
		t.Fatalf("reconcile with source: %v", err)
	}

	if result.RequestedAt == "" {
		t.Fatal("no timestamp was reported back to poll on")
	}
	source, getErr := client.
		Resource(schema.GroupVersionResource{Group: "source.toolkit.fluxcd.io", Version: "v1", Resource: "gitrepositories"}).
		Namespace("flux-system").
		Get(t.Context(), "flux-system", metav1.GetOptions{})
	if getErr != nil {
		t.Fatalf("read the source back: %v", getErr)
	}
	if source.GetAnnotations()[reconcileAnnotation] != result.RequestedAt {
		t.Fatal("the source was not asked to reconcile")
	}
	object, _ := client.Resource(kustomizationGVR).Namespace("flux-system").Get(t.Context(), "apps", metav1.GetOptions{})
	if object.GetAnnotations()[reconcileAnnotation] != result.RequestedAt {
		t.Fatal("the object itself was not asked to reconcile")
	}
}

func TestReconcileWithSourceRefusesAnObjectWithNoSource(t *testing.T) {
	client := sourceClient(t, newKustomization(false))

	_, err := Do(t.Context(), client, sourceDescs(), kustomizationRef(), ReconcileSource, stamp)

	if !errors.Is(err, ErrNoSource) {
		t.Fatalf("error = %v, want a refusal", err)
	}
}

func TestReconcileWithSourceRefusesASourceKindTheClusterDoesNotServe(t *testing.T) {
	client := sourceClient(t, sourcedKustomization(), newSource())

	_, err := Do(t.Context(), client, map[string]api.ResourceDescriptor{}, kustomizationRef(), ReconcileSource, stamp)

	if !errors.Is(err, ErrNoSource) {
		t.Fatalf("error = %v, want a refusal", err)
	}
}

func TestReconcileWithSourceReportsAnObjectThatIsNotThere(t *testing.T) {
	client := sourceClient(t)

	_, err := Do(t.Context(), client, sourceDescs(), kustomizationRef(), ReconcileSource, stamp)

	if err == nil {
		t.Fatal("expected an error for an object that is not there")
	}
}