Ctrl K
ComunidadGuías…

internal/argocd/dashboard_test.go

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

Archivo original ↗Preguntar a KiBot
package argocd

import (
	"context"
	"errors"
	"maps"
	"testing"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

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

type stubLister struct {
	items  map[string][]*unstructured.Unstructured
	errs   map[string]error
	warmed []api.ResourceDescriptor
}

func (s *stubLister) List(
	_ context.Context,
	desc api.ResourceDescriptor,
) ([]*unstructured.Unstructured, error) {
	err, refused := s.errs[desc.Resource]
	if refused {
		return nil, err
	}
	return s.items[desc.Resource], nil
}

func (s *stubLister) Warm(_ context.Context, descs []api.ResourceDescriptor) {
	s.warmed = append(s.warmed, descs...)
}

func appDescriptor() api.ResourceDescriptor {
	return api.ResourceDescriptor{
		Group:      Group,
		Version:    "v1alpha1",
		Resource:   applications,
		Kind:       "Application",
		Namespaced: true,
	}
}

func setDescriptor() api.ResourceDescriptor {
	return api.ResourceDescriptor{
		Group:      Group,
		Version:    "v1alpha1",
		Resource:   applicationSets,
		Kind:       "ApplicationSet",
		Namespaced: true,
	}
}

func projectDescriptor() api.ResourceDescriptor {
	return api.ResourceDescriptor{
		Group:      Group,
		Version:    "v1alpha1",
		Resource:   appProjects,
		Kind:       "AppProject",
		Namespaced: true,
	}
}

func catalog(descs ...api.ResourceDescriptor) map[string]api.ResourceDescriptor {
	out := map[string]api.ResourceDescriptor{}
	for _, desc := range descs {
		out[desc.Group+"/"+desc.Version+"/"+desc.Resource] = desc
	}
	out["apps/v1/deployments"] = api.ResourceDescriptor{
		Group: "apps", Version: "v1", Resource: "deployments", Kind: "Deployment",
	}
	return out
}

func app(name string, fields map[string]any) *unstructured.Unstructured {
	object := map[string]any{
		"apiVersion": "argoproj.io/v1alpha1",
		"kind":       "Application",
		"metadata":   map[string]any{"name": name, "namespace": "argocd"},
	}
	maps.Copy(object, fields)
	return &unstructured.Unstructured{Object: object}
}

func healthy(name, revision string) *unstructured.Unstructured {
	return app(name, map[string]any{
		"spec": map[string]any{
			"project":     "default",
			"source":      map[string]any{"repoURL": "https://git/apps", "path": "apps/" + name},
			"destination": map[string]any{"server": "in-cluster", "namespace": name},
		},
		"status": map[string]any{
			"sync":   map[string]any{"status": "Synced", "revision": revision},
			"health": map[string]any{"status": "Healthy"},
		},
	})
}

func onlyApps(items ...*unstructured.Unstructured) *stubLister {
	return &stubLister{items: map[string][]*unstructured.Unstructured{applications: items}}
}

func build(t *testing.T, lister Lister, descs map[string]api.ResourceDescriptor) api.ArgoDashboard {
	t.Helper()
	return Build(context.Background(), lister, descs)
}

func TestABuildWithoutArgoTypesAsksForNothing(t *testing.T) {
	lister := onlyApps(healthy("web", "abc123"))

	found := build(t, lister, catalog())

	if len(found.Apps) != 0 {
		t.Fatalf("apps = %+v, want none", found.Apps)
	}
	if len(lister.warmed) != 0 {
		t.Fatalf("warmed %d types on a cluster with no argo", len(lister.warmed))
	}
}

func TestArgoTypesOutsideTheDashboardAreNotListed(t *testing.T) {
	descs := catalog(api.ResourceDescriptor{
		Group: Group, Version: "v1alpha1", Resource: "appprojecttokens", Kind: "AppProjectToken",
	})

	if got := wanted(descs); len(got) != 0 {
		t.Fatalf("wanted = %+v, want no unsupported Argo types", got)
	}
}

func TestApplicationsCarryTheirSyncAndHealth(t *testing.T) {
	lister := onlyApps(healthy("web", "abc123"))

	found := build(t, lister, catalog(appDescriptor()))

	if len(found.Apps) != 1 {
		t.Fatalf("apps = %+v, want one", found.Apps)
	}
	got := found.Apps[0]
	if got.Sync != "Synced" || got.Health != "Healthy" {
		t.Fatalf("status = %s/%s", got.Sync, got.Health)
	}
	if got.Project != "default" || got.Repo != "https://git/apps" || got.Path != "apps/web" {
		t.Fatalf("app = %+v", got)
	}
	if got.Destination != "in-cluster web" {
		t.Fatalf("destination = %q", got.Destination)
	}
	if got.Revision != "abc123" {
		t.Fatalf("revision = %q", got.Revision)
	}
	if got.Kind != "Application" {
		t.Fatalf("kind = %q", got.Kind)
	}
}

func TestApplicationsComeBackInAStableOrder(t *testing.T) {
	first := healthy("web", "a")
	first.SetNamespace("team-b")
	second := healthy("api", "b")
	second.SetNamespace("team-a")
	lister := onlyApps(first, second)

	found := build(t, lister, catalog(appDescriptor()))

	if found.Apps[0].Name != "api" || found.Apps[1].Name != "web" {
		t.Fatalf("order = %s, %s", found.Apps[0].Name, found.Apps[1].Name)
	}
}

func TestAnAppOfAppsChildNamesItsParent(t *testing.T) {
	parent := healthy("root", "a")
	child := healthy("web", "b")
	child.SetLabels(map[string]string{trackingLabel: "root"})
	lister := onlyApps(parent, child)

	found := build(t, lister, catalog(appDescriptor()))

	for _, got := range found.Apps {
		if got.Name == "web" && got.Owner != "root" {
			t.Fatalf("owner = %q, want root", got.Owner)
		}
		if got.Name == "root" && got.Owner != "" {
			t.Fatalf("the parent claims an owner: %q", got.Owner)
		}
	}
}

func TestTheTrackingAnnotationAlsoNamesTheParent(t *testing.T) {
	parent := healthy("root", "a")
	child := healthy("web", "b")
	child.SetAnnotations(map[string]string{
		trackingAnnotation: "root:argoproj.io/Application:argocd/web",
	})
	lister := onlyApps(parent, child)

	found := build(t, lister, catalog(appDescriptor()))

	for _, got := range found.Apps {
		if got.Name == "web" && got.Owner != "root" {
			t.Fatalf("owner = %q, want root", got.Owner)
		}
	}
}

func TestAnAppTrackedByItselfHasNoParent(t *testing.T) {
	only := healthy("web", "a")
	only.SetLabels(map[string]string{trackingLabel: "web"})
	lister := onlyApps(only)

	found := build(t, lister, catalog(appDescriptor()))

	if found.Apps[0].Owner != "" {
		t.Fatalf("owner = %q, want none", found.Apps[0].Owner)
	}
}

func TestAParentThatIsNotAnApplicationIsDropped(t *testing.T) {
	child := healthy("web", "a")
	child.SetLabels(map[string]string{trackingLabel: "some-helm-release"})
	lister := onlyApps(child)

	found := build(t, lister, catalog(appDescriptor()))

	if found.Apps[0].Owner != "" {
		t.Fatalf("owner = %q, want none for a parent that is not an app", found.Apps[0].Owner)
	}
}

func TestAGeneratedAppNamesItsApplicationSet(t *testing.T) {
	child := healthy("web", "a")
	child.SetOwnerReferences([]metav1.OwnerReference{{Kind: "ApplicationSet", Name: "fleet"}})
	lister := &stubLister{
		items: map[string][]*unstructured.Unstructured{
			applications:    {child},
			applicationSets: {app("fleet", map[string]any{})},
		},
	}

	found := build(t, lister, catalog(appDescriptor(), setDescriptor()))

	if found.Apps[0].Owner != "fleet" {
		t.Fatalf("owner = %q, want the application set", found.Apps[0].Owner)
	}
	if len(found.ApplicationSets) != 1 {
		t.Fatalf("sets = %+v, want one", found.ApplicationSets)
	}
}

func TestAnAppWithSeveralSourcesStillNamesOne(t *testing.T) {
	many := app("web", map[string]any{
		"spec": map[string]any{
			"sources": []any{
				map[string]any{"repoURL": "https://git/values", "targetRevision": "main"},
				map[string]any{"repoURL": "https://git/chart", "chart": "podinfo"},
			},
		},
	})
	lister := onlyApps(many)

	found := build(t, lister, catalog(appDescriptor()))

	if found.Apps[0].Repo != "https://git/values" {
		t.Fatalf("repo = %q", found.Apps[0].Repo)
	}
	if found.Apps[0].Path != "podinfo" {
		t.Fatalf("path = %q, want the chart when there is no path", found.Apps[0].Path)
	}
	if found.Apps[0].Revision != "main" {
		t.Fatalf("revision = %q, want the target when nothing synced yet", found.Apps[0].Revision)
	}
}

func TestAnAppWithNoStatusIsStillListed(t *testing.T) {
	bare := app("web", map[string]any{"spec": map[string]any{"project": "default"}})
	lister := onlyApps(bare)

	found := build(t, lister, catalog(appDescriptor()))

	if len(found.Apps) != 1 {
		t.Fatalf("apps = %+v, want the app", found.Apps)
	}
	if found.Apps[0].Sync != "" || found.Apps[0].Health != "" {
		t.Fatalf("app = %+v, want empty status", found.Apps[0])
	}
}

func TestAHealthMessageIsCarried(t *testing.T) {
	sick := app("web", map[string]any{
		"status": map[string]any{
			"health": map[string]any{"status": "Degraded", "message": "pod crashlooping"},
		},
	})
	lister := onlyApps(sick)

	found := build(t, lister, catalog(appDescriptor()))

	if found.Apps[0].Message != "pod crashlooping" {
		t.Fatalf("message = %q", found.Apps[0].Message)
	}
}

func TestAnOperationMessageStandsInForHealth(t *testing.T) {
	failing := app("web", map[string]any{
		"status": map[string]any{
			"operationState": map[string]any{"message": "one or more objects failed"},
		},
	})
	lister := onlyApps(failing)

	found := build(t, lister, catalog(appDescriptor()))

	if found.Apps[0].Message != "one or more objects failed" {
		t.Fatalf("message = %q", found.Apps[0].Message)
	}
}

func TestADestinationWithoutANamespaceIsJustTheServer(t *testing.T) {
	remote := app("web", map[string]any{
		"spec": map[string]any{"destination": map[string]any{"server": "https://kube"}},
	})
	lister := onlyApps(remote)

	found := build(t, lister, catalog(appDescriptor()))

	if found.Apps[0].Destination != "https://kube" {
		t.Fatalf("destination = %q", found.Apps[0].Destination)
	}
}

func TestADestinationWithoutAServerIsJustTheNamespace(t *testing.T) {
	local := app("web", map[string]any{
		"spec": map[string]any{"destination": map[string]any{"namespace": "shop"}},
	})
	lister := onlyApps(local)

	found := build(t, lister, catalog(appDescriptor()))

	if found.Apps[0].Destination != "shop" {
		t.Fatalf("destination = %q", found.Apps[0].Destination)
	}
}

func TestAKindThatCannotBeListedIsReported(t *testing.T) {
	lister := &stubLister{errs: map[string]error{applications: errors.New("applications is forbidden")}}

	found := build(t, lister, catalog(appDescriptor()))

	if found.Error == "" {
		t.Fatal("a refusal was swallowed")
	}
	if len(found.Apps) != 0 {
		t.Fatalf("apps = %+v, want none", found.Apps)
	}
}

func TestApplicationsStillArriveWhenTheSetsAreRefused(t *testing.T) {
	lister := &stubLister{
		items: map[string][]*unstructured.Unstructured{applications: {healthy("web", "a")}},
		errs:  map[string]error{applicationSets: errors.New("applicationsets is forbidden")},
	}

	found := build(t, lister, catalog(appDescriptor(), setDescriptor()))

	if len(found.Apps) != 1 {
		t.Fatalf("apps = %+v, want the application", found.Apps)
	}
	if found.Error == "" {
		t.Fatal("the refusal was swallowed")
	}
}

func TestProjectsComeBackOnTheirOwn(t *testing.T) {
	lister := &stubLister{items: map[string][]*unstructured.Unstructured{
		applications: {healthy("web", "a")},
		appProjects:  {app("default", nil), app("infra", nil)},
	}}

	found := build(t, lister, catalog(appDescriptor(), projectDescriptor()))

	if len(found.Projects) != 2 {
		t.Fatalf("projects = %+v, want two", found.Projects)
	}
	if found.Projects[0].Name != "default" || found.Projects[1].Name != "infra" {
		t.Fatalf("projects came back out of order: %+v", found.Projects)
	}
	if found.Projects[0].Kind != "AppProject" {
		t.Fatalf("kind = %q", found.Projects[0].Kind)
	}
	if len(found.Apps) != 1 {
		t.Fatalf("apps = %+v, want the application", found.Apps)
	}
}

func TestProjectsAreNotParentsOfApplications(t *testing.T) {
	child := healthy("web", "a")
	child.SetLabels(map[string]string{trackingLabel: "default"})
	lister := &stubLister{items: map[string][]*unstructured.Unstructured{
		applications: {child},
		appProjects:  {app("default", nil)},
	}}

	found := build(t, lister, catalog(appDescriptor(), projectDescriptor()))

	if found.Apps[0].Owner != "" {
		t.Fatalf("owner = %q, want the project ignored", found.Apps[0].Owner)
	}
}

func TestOnlyArgoTypesAreWarmed(t *testing.T) {
	lister := onlyApps(healthy("web", "a"))

	build(t, lister, catalog(appDescriptor(), setDescriptor()))

	for _, desc := range lister.warmed {
		if desc.Group != Group {
			t.Fatalf("warmed %s, which is not argo", desc.Resource)
		}
	}
	if len(lister.warmed) != 2 {
		t.Fatalf("warmed %d types, want applications and applicationsets", len(lister.warmed))
	}
}

func TestInstalledFollowsTheApplicationType(t *testing.T) {
	if Installed(catalog()) {
		t.Fatal("argo was reported on a cluster without it")
	}
	if !Installed(catalog(appDescriptor())) {
		t.Fatal("argo was not reported on a cluster with applications")
	}
}

func TestOnlyTheThreeArgoKindsAreWanted(t *testing.T) {
	if listed("rollouts") {
		t.Fatal("rollouts is listed; only applications, sets and projects are")
	}
	if !listed(applications) || !listed(applicationSets) || !listed(appProjects) {
		t.Fatal("one of the three argo kinds went missing")
	}
}

func TestASourceIsReadFromTheListWhenThereIsNoSingleOne(t *testing.T) {
	item := &unstructured.Unstructured{Object: map[string]any{
		"spec": map[string]any{
			"sources": []any{
				"not a source",
				map[string]any{"repoURL": ""},
				map[string]any{"repoURL": "https://example.com/apps.git"},
			},
		},
	}}

	if got := sourceOf(item, "repoURL"); got != "https://example.com/apps.git" {
		t.Fatalf("source = %q, want the first usable one in the list", got)
	}
}

func TestASourceIsEmptyWhenNoneOfTheListHasIt(t *testing.T) {
	item := &unstructured.Unstructured{Object: map[string]any{
		"spec": map[string]any{"sources": []any{map[string]any{"path": "apps"}}},
	}}

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

func TestATrackingIDOnlyNamesSomebodyElse(t *testing.T) {
	cases := []struct {
		name string
		id   string
		self string
		want string
	}{
		{name: "nothing tracked", id: "", self: "guestbook", want: ""},
		{name: "no owner in the id", id: "guestbook", self: "guestbook", want: ""},
		{name: "tracked by itself", id: "guestbook:apps/Deployment", self: "guestbook", want: ""},
		{name: "tracked by another app", id: "root:apps/Deployment", self: "guestbook", want: "root"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := fromTrackingID(tc.id, tc.self); got != tc.want {
				t.Fatalf("owner = %q, want %q", got, tc.want)
			}
		})
	}
}

func TestTheDashboardNamesHowAnApplicationAutomates(t *testing.T) {
	app := automating(newApplication(), map[string]any{"prune": true, "selfHeal": true})
	desc := api.ResourceDescriptor{Group: Group, Version: "v1alpha1", Resource: applications, Kind: "Application"}

	if got := automationOf(app, desc); got != "auto · prune, self-heal" {
		t.Fatalf("automation = %q", got)
	}
}

func TestTheDashboardCallsAPausedApplicationSuspended(t *testing.T) {
	app := automating(newApplication(), map[string]any{"enabled": false})
	desc := api.ResourceDescriptor{Group: Group, Version: "v1alpha1", Resource: applications, Kind: "Application"}

	if got := automationOf(app, desc); got != "suspended · neither prune nor self-heal" {
		t.Fatalf("automation = %q", got)
	}
}

func TestTheDashboardCallsAnApplicationWithNoPolicyManual(t *testing.T) {
	desc := api.ResourceDescriptor{Group: Group, Version: "v1alpha1", Resource: applications, Kind: "Application"}

	if got := automationOf(newApplication(), desc); got != "manual" {
		t.Fatalf("automation = %q", got)
	}
}

func TestTheDashboardLeavesProjectsAndSetsWithoutAPolicy(t *testing.T) {
	desc := api.ResourceDescriptor{Group: Group, Version: "v1alpha1", Resource: appProjects, Kind: "AppProject"}

	if got := automationOf(newApplication(), desc); got != "" {
		t.Fatalf("automation = %q, want nothing", got)
	}
}