package deployer import ( "context" "testing" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func newTestDeployer(client *fake.Clientset) *Deployer { return NewDeployer(client, Config{Namespace: "projects"}) } func createFakeDeployment(t *testing.T, client *fake.Clientset, name, project string) { t.Helper() replicas := int32(1) _, err := client.AppsV1().Deployments("projects").Create(context.Background(), &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: "projects", Labels: map[string]string{"app": name, "project": project}, }, Spec: appsv1.DeploymentSpec{ Replicas: &replicas, Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": name}}, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": name}}, Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: name, Image: "test:latest"}}}, }, }, }, metav1.CreateOptions{}) if err != nil { t.Fatalf("failed to create deployment %s: %v", name, err) } } func createFakeService(t *testing.T, client *fake.Clientset, name, project string) { t.Helper() _, err := client.CoreV1().Services("projects").Create(context.Background(), &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: "projects", Labels: map[string]string{"app": name, "project": project}, }, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": name}, Ports: []corev1.ServicePort{{Port: 8080}}, }, }, metav1.CreateOptions{}) if err != nil { t.Fatalf("failed to create service %s: %v", name, err) } } func createFakeIngress(t *testing.T, client *fake.Clientset, name, project string) { t.Helper() _, err := client.NetworkingV1().Ingresses("projects").Create(context.Background(), &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: "projects", Labels: map[string]string{"app": name, "project": project}, }, }, metav1.CreateOptions{}) if err != nil { t.Fatalf("failed to create ingress %s: %v", name, err) } } func createFakeSecret(t *testing.T, client *fake.Clientset, name, project string) { t.Helper() _, err := client.CoreV1().Secrets("projects").Create(context.Background(), &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: "projects", Labels: map[string]string{"project": project}, }, }, metav1.CreateOptions{}) if err != nil { t.Fatalf("failed to create secret %s: %v", name, err) } } func TestUndeployAll_SingleApp(t *testing.T) { client := fake.NewSimpleClientset() d := newTestDeployer(client) ctx := context.Background() createFakeDeployment(t, client, "myapp", "myapp") createFakeService(t, client, "myapp", "myapp") createFakeIngress(t, client, "myapp", "myapp") createFakeSecret(t, client, "myapp-env", "myapp") if err := d.UndeployAll(ctx, "myapp"); err != nil { t.Fatalf("UndeployAll failed: %v", err) } deps, _ := client.AppsV1().Deployments("projects").List(ctx, metav1.ListOptions{}) if len(deps.Items) != 0 { t.Errorf("expected 0 deployments, got %d", len(deps.Items)) } svcs, _ := client.CoreV1().Services("projects").List(ctx, metav1.ListOptions{}) if len(svcs.Items) != 0 { t.Errorf("expected 0 services, got %d", len(svcs.Items)) } ings, _ := client.NetworkingV1().Ingresses("projects").List(ctx, metav1.ListOptions{}) if len(ings.Items) != 0 { t.Errorf("expected 0 ingresses, got %d", len(ings.Items)) } secs, _ := client.CoreV1().Secrets("projects").List(ctx, metav1.ListOptions{}) if len(secs.Items) != 0 { t.Errorf("expected 0 secrets, got %d", len(secs.Items)) } } func TestUndeployAll_MonorepoComponents(t *testing.T) { client := fake.NewSimpleClientset() d := newTestDeployer(client) ctx := context.Background() // Main project + two components all sharing "project=myapp" label createFakeDeployment(t, client, "myapp", "myapp") createFakeDeployment(t, client, "myapp-studio-ui", "myapp") createFakeDeployment(t, client, "myapp-studio-api", "myapp") createFakeService(t, client, "myapp", "myapp") createFakeService(t, client, "myapp-studio-ui", "myapp") createFakeService(t, client, "myapp-studio-api", "myapp") createFakeIngress(t, client, "myapp", "myapp") // Also create a resource for a DIFFERENT project - should not be deleted createFakeDeployment(t, client, "other-project", "other-project") createFakeService(t, client, "other-project", "other-project") if err := d.UndeployAll(ctx, "myapp"); err != nil { t.Fatalf("UndeployAll failed: %v", err) } deps, _ := client.AppsV1().Deployments("projects").List(ctx, metav1.ListOptions{}) if len(deps.Items) != 1 { t.Errorf("expected 1 deployment (other-project), got %d", len(deps.Items)) } if len(deps.Items) == 1 && deps.Items[0].Name != "other-project" { t.Errorf("remaining deployment = %q, want %q", deps.Items[0].Name, "other-project") } svcs, _ := client.CoreV1().Services("projects").List(ctx, metav1.ListOptions{}) if len(svcs.Items) != 1 { t.Errorf("expected 1 service (other-project), got %d", len(svcs.Items)) } ings, _ := client.NetworkingV1().Ingresses("projects").List(ctx, metav1.ListOptions{}) if len(ings.Items) != 0 { t.Errorf("expected 0 ingresses, got %d", len(ings.Items)) } } func TestUndeployAll_IdempotentNoOp(t *testing.T) { client := fake.NewSimpleClientset() d := newTestDeployer(client) ctx := context.Background() // No resources exist for this project if err := d.UndeployAll(ctx, "nonexistent"); err != nil { t.Fatalf("UndeployAll should be idempotent, got: %v", err) } } func TestListProjectLabels(t *testing.T) { client := fake.NewSimpleClientset() d := newTestDeployer(client) ctx := context.Background() createFakeDeployment(t, client, "proj-a", "proj-a") createFakeDeployment(t, client, "proj-b", "proj-b") createFakeDeployment(t, client, "proj-b-ui", "proj-b") createFakeDeployment(t, client, "proj-b-api", "proj-b") labels, err := d.ListProjectLabels(ctx) if err != nil { t.Fatalf("ListProjectLabels failed: %v", err) } // Should have exactly 2 unique labels if len(labels) != 2 { t.Fatalf("expected 2 unique labels, got %d: %v", len(labels), labels) } found := make(map[string]bool) for _, l := range labels { found[l] = true } if !found["proj-a"] || !found["proj-b"] { t.Errorf("expected proj-a and proj-b, got %v", labels) } } func TestListProjectLabels_Empty(t *testing.T) { client := fake.NewSimpleClientset() d := newTestDeployer(client) labels, err := d.ListProjectLabels(context.Background()) if err != nil { t.Fatalf("ListProjectLabels failed: %v", err) } if len(labels) != 0 { t.Errorf("expected 0 labels, got %d", len(labels)) } } func TestGetOldestResourceTime(t *testing.T) { client := fake.NewSimpleClientset() d := newTestDeployer(client) ctx := context.Background() createFakeDeployment(t, client, "myapp", "myapp") createFakeDeployment(t, client, "myapp-ui", "myapp") _, found, err := d.GetOldestResourceTime(ctx, "myapp") if err != nil { t.Fatalf("GetOldestResourceTime failed: %v", err) } if !found { t.Fatal("expected found=true") } } func TestGetOldestResourceTime_NoResources(t *testing.T) { client := fake.NewSimpleClientset() d := newTestDeployer(client) _, found, err := d.GetOldestResourceTime(context.Background(), "nonexistent") if err != nil { t.Fatalf("GetOldestResourceTime failed: %v", err) } if found { t.Error("expected found=false for nonexistent project") } }