package bootstrap import ( "bytes" "io" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/Pingu-Studio/MaidnCLI/internal/config" "github.com/Pingu-Studio/MaidnCLI/internal/forgejo" "gopkg.in/yaml.v3" ) func onboardingGit(t *testing.T, dir string, args ...string) string { t.Helper() command := exec.Command("git", args...) command.Dir = dir output, err := command.CombinedOutput() if err != nil { t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, output) } return strings.TrimSpace(string(output)) } func onboardingConfig() config.Config { return config.Config{ Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "test-org-2"}, Flux: config.FluxConfig{Branch: "main", RepoName: "cluster", ManifestsRepo: "manifests", ClusterDomain: "example.test"}, Delivery: config.DeliveryConfig{ AppName: "web-ui", AppRepoURL: "https://git.example.test/test-org-2/web-ui.git", AppRepoRef: "main", ProductionBranch: "production", ImageRepository: "registry.example.test/test-org-2/web-ui", BuildStrategy: "static", BuildOutputDirectory: "dist", BuildConfiguration: "production", WebhookHostname: "tekton.example.test", WebhookPath: "/", }, } } func TestGenerateAppDeliveryReplacesOnlyKnownGeneratedFiles(t *testing.T) { dir := t.TempDir() tektonDir := filepath.Join(dir, ".tekton") if err := os.Mkdir(tektonDir, 0755); err != nil { t.Fatal(err) } for _, name := range []string{"kustomization.yaml", "pipeline.yaml"} { if err := os.WriteFile(filepath.Join(tektonDir, name), []byte("old generated content\n"), 0644); err != nil { t.Fatal(err) } } if err := GenerateAppDelivery(dir, onboardingConfig()); err != nil { t.Fatal(err) } pipeline, err := os.ReadFile(filepath.Join(tektonDir, "pipeline.yaml")) normalized := strings.ReplaceAll(string(pipeline), "\r\n", "\n") if err != nil || !strings.Contains(normalized, "https://git.example.test/test-org-2/web-ui.git") || !strings.Contains(normalized, "name: HOME\n value: /tekton/home") || !strings.Contains(normalized, "grep -qxF \" namespace: staging\"") || !strings.Contains(normalized, "namespace: production") || !strings.Contains(normalized, "name: web-ui-wait-delivery") || !strings.Contains(normalized, "name: web-ui-report-delivery") || !strings.Contains(normalized, "forgejo-delivery-status") || !strings.Contains(normalized, "$(context.pipelineRun.name)") || !strings.Contains(normalized, "Promotion PR opened or updated") || strings.Contains(normalized, "taskRunTemplate:") { t.Fatalf("target-specific pipeline = %q, %v", pipeline, err) } decoder := yaml.NewDecoder(bytes.NewReader(pipeline)) for { var document yaml.Node err := decoder.Decode(&document) if err == io.EOF { break } if err != nil { t.Fatalf("generated pipeline YAML: %v", err) } } if err := os.WriteFile(filepath.Join(tektonDir, "custom.yaml"), []byte("custom: true\n"), 0644); err != nil { t.Fatal(err) } if err := GenerateAppDelivery(dir, onboardingConfig()); err == nil || !strings.Contains(err.Error(), "unmanaged") { t.Fatalf("custom .tekton content was accepted: %v", err) } } func TestRegisterAppInClusterRendersManagedFluxSource(t *testing.T) { dir := t.TempDir() tektonDir := filepath.Join(dir, "base", "tekton") if err := os.MkdirAll(tektonDir, 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(tektonDir, "kustomization.yaml"), []byte("apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n"), 0644); err != nil { t.Fatal(err) } if err := RegisterAppInCluster(dir, onboardingConfig()); err != nil { t.Fatal(err) } registration, err := os.ReadFile(filepath.Join(tektonDir, "apps", "web-ui.yaml")) if err != nil || !strings.Contains(string(registration), "branch: maidn/delivery-web-ui") || !strings.Contains(string(registration), "secretRef:\n name: forgejo-flux-credentials") || !strings.Contains(string(registration), "dependsOn:\n - name: tekton-catalog") || !strings.Contains(string(registration), "path: ./.tekton") { t.Fatalf("registration = %q, %v", registration, err) } for path, resource := range map[string]string{filepath.Join(tektonDir, "kustomization.yaml"): "apps", filepath.Join(tektonDir, "apps", "kustomization.yaml"): "web-ui.yaml"} { content, err := os.ReadFile(path) if err != nil || !strings.Contains(string(content), resource) { t.Fatalf("Kustomization %s does not include %s: %q, %v", path, resource, content, err) } } if err := os.WriteFile(filepath.Join(tektonDir, "apps", "web-ui.yaml"), bytes.ReplaceAll(registration, []byte("\n"), []byte("\r\n")), 0644); err != nil { t.Fatal(err) } if err := RegisterAppInCluster(dir, onboardingConfig()); err != nil { t.Fatalf("CRLF registration was rejected: %v", err) } if err := os.WriteFile(filepath.Join(tektonDir, "apps", "web-ui.yaml"), []byte("custom: true\n"), 0644); err != nil { t.Fatal(err) } if err := RegisterAppInCluster(dir, onboardingConfig()); err == nil || !strings.Contains(err.Error(), "conflicts") { t.Fatalf("unmanaged app registration was accepted: %v", err) } } func TestGenerateAppSecretAccessRendersOnlyDeclaredRuntimeSecrets(t *testing.T) { dir := t.TempDir() cfg := onboardingConfig() cfg.SecretGrants = []config.SecretGrant{{Application: "web-ui", Consumer: "runtime", Environment: "staging", Secrets: []string{"api-key"}, Shared: []string{"payments"}}} if err := GenerateAppSecretAccess(dir, cfg); err != nil { t.Fatal(err) } access, err := os.ReadFile(filepath.Join(dir, ".maidn", "secret-access.yaml")) if err != nil || !strings.Contains(string(access), "namespace: staging") || !strings.Contains(string(access), "key: apps/web-ui/api-key") || strings.Contains(string(access), "shared/payments") { t.Fatalf("secret access = %q, %v", access, err) } registration, err := renderAppRegistration(cfg) if err != nil || !strings.Contains(string(registration), "name: web-ui-secrets") || !strings.Contains(string(registration), "path: ./.maidn") { t.Fatalf("secret registration = %q, %v", registration, err) } } func TestPublishInitialAppBranchesCreatesAndPreservesProduction(t *testing.T) { source := filepath.Join(t.TempDir(), "source") target := filepath.Join(t.TempDir(), "target.git") if err := os.Mkdir(source, 0755); err != nil { t.Fatal(err) } onboardingGit(t, source, "init", "-b", "source") onboardingGit(t, source, "config", "user.name", "Test") onboardingGit(t, source, "config", "user.email", "test@example.test") if err := os.WriteFile(filepath.Join(source, "README.md"), []byte("source\n"), 0644); err != nil { t.Fatal(err) } onboardingGit(t, source, "add", "README.md") onboardingGit(t, source, "commit", "-m", "source") sourceRevision := onboardingGit(t, source, "rev-parse", "source") onboardingGit(t, "", "init", "--bare", target) manager := forgejo.NewRepoManager("https://git.example.test", "", "owner", "", "", "", "main", "") if err := publishInitialAppBranches(manager, source, target, "source", "main", "production"); err != nil { t.Fatal(err) } for _, branch := range []string{"main", "production"} { if got := onboardingGit(t, "", "--git-dir", target, "rev-parse", "refs/heads/"+branch); got != sourceRevision { t.Fatalf("%s = %s, want source %s", branch, got, sourceRevision) } } preservedTarget := filepath.Join(t.TempDir(), "preserved-target.git") onboardingGit(t, "", "init", "--bare", preservedTarget) onboardingGit(t, source, "push", preservedTarget, "source:main") production := filepath.Join(t.TempDir(), "production") if err := os.Mkdir(production, 0755); err != nil { t.Fatal(err) } onboardingGit(t, production, "init", "-b", "production") onboardingGit(t, production, "config", "user.name", "Test") onboardingGit(t, production, "config", "user.email", "test@example.test") if err := os.WriteFile(filepath.Join(production, "README.md"), []byte("existing production\n"), 0644); err != nil { t.Fatal(err) } onboardingGit(t, production, "add", "README.md") onboardingGit(t, production, "commit", "-m", "existing production") existingProduction := onboardingGit(t, production, "rev-parse", "production") onboardingGit(t, production, "push", preservedTarget, "production:production") if err := publishInitialAppBranches(manager, source, preservedTarget, "source", "main", "production"); err != nil { t.Fatal(err) } if got := onboardingGit(t, "", "--git-dir", preservedTarget, "rev-parse", "refs/heads/production"); got != existingProduction { t.Fatalf("production = %s, want existing %s", got, existingProduction) } if err := os.WriteFile(filepath.Join(source, "README.md"), []byte("updated source\n"), 0644); err != nil { t.Fatal(err) } onboardingGit(t, source, "commit", "-am", "updated source") if err := publishInitialAppBranches(manager, source, preservedTarget, "source", "main", "production"); err != nil { t.Fatal(err) } if got := onboardingGit(t, "", "--git-dir", preservedTarget, "rev-parse", "refs/heads/main"); got != sourceRevision { t.Fatalf("main = %s, want existing %s", got, sourceRevision) } }