package bootstrap import ( "errors" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" "strings" "testing" "github.com/Pingu-Studio/MaidnCLI/internal/config" "github.com/Pingu-Studio/MaidnCLI/internal/forgejo" ) func freshPlanConfig(t *testing.T) config.Config { t.Helper() workspace := t.TempDir() return config.Config{ WorkspaceDir: workspace, Git: config.GitConfig{Provider: "forgejo", BaseURL: "https://git.example.test", Username: "bot", Token: "token", CloneParent: filepath.Join(workspace, "checkouts")}, Flux: config.FluxConfig{RepoName: "cluster", Branch: "main", ClusterPath: "./clusters/cluster", ClusterDomain: "cluster.example.test", ManifestsRepo: "manifests", TektonCatalogRepo: "catalog"}, Talos: config.TalosConfig{RepoDirName: "talos", GeneratedDir: "generated"}, Delivery: config.DeliveryConfig{AppName: "app", AppRepoURL: "https://git.example.test/new-org/app.git", AppRepoRef: "main", ProductionBranch: "production", ImageRepository: "registry.example.test/new-org/app", BuildOutputDirectory: "dist", BuildConfiguration: "production", WebhookHostname: "tekton.cluster.example.test", WebhookPath: "/"}, Templates: config.TemplateConfig{ TalosRepoURL: "https://git.example.test/templates/talos.git", TalosRepoRef: "main", CICDRepoURL: "https://git.example.test/templates/cicd.git", CICDRepoRef: "main", ManifestsRepoURL: "https://git.example.test/templates/manifests.git", ManifestsRepoRef: "main", TektonCatalogRepoURL: "https://git.example.test/templates/catalog.git", TektonCatalogRepoRef: "main", }, } } func TestPlanFreshOrganizationOrdersOnlyFreshPhases(t *testing.T) { cfg := freshPlanConfig(t) resolved, plan, err := PlanFreshOrganization(cfg, FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true, EnableDelivery: true}) if err != nil { t.Fatal(err) } if resolved.Git.Owner != "new-org" { t.Fatal("organization was not bound to the fresh configuration") } want := []string{ "validate isolated workspace and configuration", "lock template revisions in the isolated workspace", "ensure the Forgejo organization", "ensure baseline Flux and manifests repositories", "initialize the user-managed Tekton catalog repository", "reconcile the CI/CD cluster", } if !reflect.DeepEqual(plan.Phases, want) { t.Fatalf("plan phases = %#v, want %#v", plan.Phases, want) } } func TestPlanFreshOrganizationUsesSelectedLifecycleMode(t *testing.T) { for _, test := range []struct { name string options FreshOrganizationOptions phase string }{ {"default", FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true}, "reconcile the CI/CD cluster"}, {"rebuild", FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true, Mode: Rebuild, ConfirmRebuild: true}, "rebuild the CI/CD cluster"}, } { t.Run(test.name, func(t *testing.T) { _, plan, err := PlanFreshOrganization(freshPlanConfig(t), test.options) if err != nil { t.Fatal(err) } if got := plan.Phases[len(plan.Phases)-1]; got != test.phase { t.Fatalf("lifecycle phase = %q, want %q", got, test.phase) } }) } } func TestPlanFreshOrganizationRejectsUnconfirmedRebuild(t *testing.T) { _, _, err := PlanFreshOrganization(freshPlanConfig(t), FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true, Mode: Rebuild}) if err == nil || !strings.Contains(err.Error(), "--mode=rebuild --yes") { t.Fatalf("PlanFreshOrganization() error = %v", err) } } func TestRunFreshOrganizationOrdersSourceControlBeforeLifecycle(t *testing.T) { originalLock, originalManager, originalLifecycle := ensureFreshTemplateRevisions, newFreshRepoManager, runFreshLifecycle t.Cleanup(func() { ensureFreshTemplateRevisions, newFreshRepoManager, runFreshLifecycle = originalLock, originalManager, originalLifecycle }) var phases []string server := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { switch request.Method + " " + request.URL.Path { case http.MethodGet + " /api/v1/orgs/new-org": phases = append(phases, "organization lookup") writer.WriteHeader(http.StatusNotFound) case http.MethodPost + " /api/v1/orgs": if !reflect.DeepEqual(phases, []string{"template lock", "organization lookup"}) { t.Fatalf("organization creation phase order = %#v", phases) } phases = append(phases, "organization create") writer.WriteHeader(http.StatusCreated) default: t.Fatalf("unexpected Forgejo request %s %s", request.Method, request.URL.Path) } })) defer server.Close() ensureFreshTemplateRevisions = func(config.Config) error { phases = append(phases, "template lock") return nil } newFreshRepoManager = func(_ string, token, owner, username, manifestsRepo, fluxRepo, branch, migrationBranch string) *forgejo.RepoManager { manager := forgejo.NewRepoManager(server.URL, token, owner, username, manifestsRepo, fluxRepo, branch, migrationBranch) manager.HTTPClient = server.Client() return manager } runFreshLifecycle = func(cfg config.Config, options FreshOrganizationOptions) error { if !options.EnableDelivery || cfg.Git.Owner != "new-org" || !reflect.DeepEqual(phases, []string{"template lock", "organization lookup", "organization create"}) { t.Fatal("lifecycle ran before the locked Forgejo source-control preflight") } phases = append(phases, "lifecycle") return nil } if _, err := RunFreshOrganization(freshPlanConfig(t), FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true, EnableDelivery: true}); err != nil { t.Fatal(err) } if !reflect.DeepEqual(phases, []string{"template lock", "organization lookup", "organization create", "lifecycle"}) { t.Fatalf("fresh bootstrap phases = %#v", phases) } } func TestRunFreshOrganizationStopsBeforeLifecycleOnPreflightFailure(t *testing.T) { originalLock, originalLifecycle := ensureFreshTemplateRevisions, runFreshLifecycle t.Cleanup(func() { ensureFreshTemplateRevisions, runFreshLifecycle = originalLock, originalLifecycle }) ensureFreshTemplateRevisions = func(config.Config) error { return errors.New("unavailable") } runFreshLifecycle = func(config.Config, FreshOrganizationOptions) error { t.Fatal("lifecycle ran after template lock failure") return nil } _, err := RunFreshOrganization(freshPlanConfig(t), FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true}) if err == nil || !strings.Contains(err.Error(), "lock template revisions") { t.Fatalf("RunFreshOrganization() error = %v", err) } } func TestRunFreshOrganizationStopsBeforeLifecycleOnForgejoFailure(t *testing.T) { originalLock, originalManager, originalLifecycle := ensureFreshTemplateRevisions, newFreshRepoManager, runFreshLifecycle t.Cleanup(func() { ensureFreshTemplateRevisions, newFreshRepoManager, runFreshLifecycle = originalLock, originalManager, originalLifecycle }) server := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusInternalServerError) })) defer server.Close() ensureFreshTemplateRevisions = func(config.Config) error { return nil } newFreshRepoManager = func(_ string, token, owner, username, manifestsRepo, fluxRepo, branch, migrationBranch string) *forgejo.RepoManager { manager := forgejo.NewRepoManager(server.URL, token, owner, username, manifestsRepo, fluxRepo, branch, migrationBranch) manager.HTTPClient = server.Client() return manager } runFreshLifecycle = func(config.Config, FreshOrganizationOptions) error { t.Fatal("lifecycle ran after Forgejo preflight failure") return nil } _, err := RunFreshOrganization(freshPlanConfig(t), FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true}) if err == nil || !strings.Contains(err.Error(), "ensure Forgejo organization") { t.Fatalf("RunFreshOrganization() error = %v", err) } } func TestFreshLifecycleUsesSelectedModeAndGatesDelivery(t *testing.T) { originalPreflight := preflight t.Cleanup(func() { preflight = originalPreflight }) cfg := runnerTestConfig(t.TempDir(), "") for _, test := range []struct { options FreshOrganizationOptions mode Mode }{ {FreshOrganizationOptions{}, Reconcile}, {FreshOrganizationOptions{Mode: Rebuild, ConfirmRebuild: true, EnableDelivery: true}, Rebuild}, } { runner := freshLifecycleRunner(cfg, test.options) if runner.Mode != test.mode || runner.ConfirmRebuild != test.options.ConfirmRebuild || runner.EnableDelivery != test.options.EnableDelivery || runner.SkipDeliveryScaffolding == test.options.EnableDelivery { t.Fatalf("fresh lifecycle runner = %#v", runner) } preflight = func(got config.Config) error { if got.Delivery.Configured() != test.options.EnableDelivery { t.Fatal("fresh lifecycle did not gate delivery scaffolding with --enable-delivery") } return errors.New("stop") } if err := runner.Run(); err == nil || !strings.Contains(err.Error(), "preflight: stop") { t.Fatalf("fresh lifecycle run = %v", err) } } } func TestPlanFreshOrganizationRejectsUnrecognizedWorkspaceState(t *testing.T) { cfg := freshPlanConfig(t) if err := os.WriteFile(filepath.Join(cfg.WorkspaceDir, "leftover"), []byte("state"), 0600); err != nil { t.Fatal(err) } if _, _, err := PlanFreshOrganization(cfg, FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true}); err == nil { t.Fatal("ambiguous workspace state was accepted") } }