package apply import ( "context" "path/filepath" "testing" "github.com/alyraffauf/cattery/internal/deployment" "github.com/alyraffauf/cattery/internal/failure" ) func TestApplyPreparation(t *testing.T) { scenarios := []struct { name string run func(*testing.T) }{ {"dry run records", testPrepareDryRun}, {"noninteractive refusal", testPrepareNoninteractive}, {"skip decision", testPrepareSkip}, {"no-op stays empty", testPrepareNoop}, {"required hooks", testPrepareHooks}, } for _, scenario := range scenarios { t.Run(scenario.name, scenario.run) } } // driftFixture evaluates one drifting target per name and returns the // service and candidates. func driftFixture(t *testing.T, targets ...string) evalPair { t.Helper() repo := t.TempDir() home := t.TempDir() files := make([]deployment.ManagedFile, 0, len(targets)) for _, name := range targets { files = append(files, ordinarySource(t, fileSpec{Repo: repo, Target: name, Relative: "files/" + name}, []byte("source "+name))) writeTarget(t, targetPath(home, name), []byte("target "+name)) } plan, err := deployment.NewPlan(deployment.PlanInput{ RepositoryRoot: repo, Platform: "linux", Files: files, Hooks: []deployment.Hook{{Scope: deployment.NewScope(""), Phase: deployment.HookBefore, Name: "b.sh", AbsolutePath: filepath.Join(repo, "_hooks", "before", "b.sh"), RepositoryPath: "_hooks/before/b.sh"}}, }) if err != nil { t.Fatalf("plan: %v", err) } service := evalFixture(t, evalInput{repo: repo, home: home, plan: plan}) candidates, err := service.Evaluate(context.Background(), evalRequest()) if err != nil { t.Fatalf("evaluate: %v", err) } return evalPair{service: service, candidates: candidates} } // resolveChoice collects the decision of the pending candidate for the // given choice. func resolveChoice(t *testing.T, pair evalPair, choice DecisionChoice) CollectedDecisions { t.Helper() pair.service.resolver = &resolverFake{responses: []DecisionResponse{{Choice: choice}}} collected, err := pair.service.CollectDecisions(context.Background(), pair.candidates) if err != nil { t.Fatalf("collect: %v", err) } return collected } // evalPair bundles one evaluated service and its candidates. type evalPair struct { service *Service candidates Candidates } func testPrepareDryRun(t *testing.T) { pair := driftFixture(t, "a.conf") plan, err := pair.service.Prepare(context.Background(), PrepareInput{Request: Request{DryRun: true}, Candidates: pair.candidates}) if err != nil { t.Fatalf("prepare: %v", err) } records := plan.Records() if len(records) != 1 || records[0].Status != StatusPlanned || records[0].Kind != ActionKindWriteSource { t.Fatalf("dry-run records = %+v, want one planned write-source record", records) } if len(plan.Actions().Items()) != 0 || plan.WithHooks() { t.Fatal("dry-run must carry no actions and no hooks") } if plan.Summary().Planned != 1 { t.Fatalf("summary = %+v, want one planned", plan.Summary()) } } func testPrepareNoninteractive(t *testing.T) { pair := driftFixture(t, "a.conf") _, err := pair.service.Prepare(context.Background(), PrepareInput{Request: Request{NonInteractive: true}, Candidates: pair.candidates}) if err == nil || !kindIs(err, failure.InvalidInput) { t.Fatalf("noninteractive refusal error = %v, want an invalid input failure", err) } plan, err := pair.service.Prepare(context.Background(), PrepareInput{Request: Request{DryRun: true, NonInteractive: true}, Candidates: pair.candidates}) if err != nil { t.Fatalf("dry-run must not refuse pending decisions: %v", err) } if len(plan.Records()) != 1 { t.Fatalf("dry-run records = %d, want 1", len(plan.Records())) } } func testPrepareSkip(t *testing.T) { pair := driftFixture(t, "a.conf") decisions := resolveChoice(t, pair, ChoiceSkip) plan, err := pair.service.Prepare(context.Background(), PrepareInput{Request: Request{}, Candidates: pair.candidates, Decisions: decisions}) if err != nil { t.Fatalf("prepare: %v", err) } records := plan.Records() if len(records) != 1 || records[0].Status != StatusPlanned { t.Fatalf("skip records = %+v, want one planned record", records) } if len(plan.Actions().Items()) != 0 { t.Fatal("a skipped plan must carry no actions") } if !plan.WithHooks() { t.Fatal("compiled hooks must still run after a skip") } } func testPrepareNoop(t *testing.T) { repo := t.TempDir() home := t.TempDir() file := ordinarySource(t, fileSpec{Repo: repo, Target: "a.conf", Relative: "files/a"}, []byte("same")) writeTarget(t, targetPath(home, "a.conf"), []byte("same")) service := evalFixture(t, evalInput{repo: repo, home: home, plan: evalPlan(t, repo, file)}) candidates, err := service.Evaluate(context.Background(), evalRequest()) if err != nil { t.Fatalf("evaluate: %v", err) } plan, err := service.Prepare(context.Background(), PrepareInput{Request: Request{}, Candidates: candidates}) if err != nil { t.Fatalf("prepare: %v", err) } if len(plan.Actions().Items()) != 0 || len(plan.Records()) != 0 || plan.WithHooks() { t.Fatal("a converged apply must stay empty") } } func testPrepareHooks(t *testing.T) { pair := driftFixture(t, "a.conf") decisions := resolveChoice(t, pair, ChoiceOverwrite) assertPreparedAction(t, pair, decisions) assertHooksSuppressed(t, pair, decisions) } // assertPreparedAction requires one write-source action and hooks for an // executing plan. func assertPreparedAction(t *testing.T, pair evalPair, decisions CollectedDecisions) { t.Helper() plan, err := pair.service.Prepare(context.Background(), PrepareInput{Request: Request{}, Candidates: pair.candidates, Decisions: decisions}) if err != nil { t.Fatalf("prepare: %v", err) } if !plan.WithHooks() { t.Fatal("an executing plan with actions must run hooks") } actions := plan.Actions().Items() if len(actions) != 1 || actions[0].Kind != ActionKindWriteSource || actions[0].TargetPath != "a.conf" { t.Fatalf("actions = %+v, want one write-source action", actions) } } // assertHooksSuppressed requires --no-hooks and dry-run to suppress hooks. func assertHooksSuppressed(t *testing.T, pair evalPair, decisions CollectedDecisions) { t.Helper() plan, err := pair.service.Prepare(context.Background(), PrepareInput{Request: Request{NoHooks: true}, Candidates: pair.candidates, Decisions: decisions}) if err != nil { t.Fatalf("prepare: %v", err) } if plan.WithHooks() { t.Fatal("--no-hooks must suppress hooks") } plan, err = pair.service.Prepare(context.Background(), PrepareInput{Request: Request{DryRun: true}, Candidates: pair.candidates}) if err != nil { t.Fatalf("prepare: %v", err) } if plan.WithHooks() { t.Fatal("dry-run must suppress hooks") } }