package cli import ( "bytes" "context" "encoding/json" "os" "strings" "testing" "git.cloudinit.dev/coreci/orca/internal/paths" "git.cloudinit.dev/coreci/orca/internal/secrets" ) // mockVerifyTransport is a record-and-replay mock of jobVerifyTransport. type mockVerifyTransport struct { writes []mockWriteCall execs []string execOut []byte execErr error } type mockWriteCall struct { peer string path string content []byte mode os.FileMode } func (m *mockVerifyTransport) WriteFileIdempotent(_ context.Context, peer string, path string, content []byte, mode os.FileMode) (bool, error) { m.writes = append(m.writes, mockWriteCall{peer, path, content, mode}) return true, nil } func (m *mockVerifyTransport) Exec(_ context.Context, _ string, cmd string) ([]byte, error) { m.execs = append(m.execs, cmd) return m.execOut, m.execErr } // setupVerifyEnv sets ORCA_HOME to a temp dir and writes a master key // (txn.RenderBundle requires it). func setupVerifyEnv(t *testing.T) string { t.Helper() dir := t.TempDir() t.Setenv("ORCA_HOME", dir) mk, err := secrets.GenerateMasterKey() if err != nil { t.Fatalf("GenerateMasterKey: %v", err) } if err := secrets.SaveMasterKey(paths.MasterKeyPath(), mk); err != nil { t.Fatalf("SaveMasterKey: %v", err) } return dir } func TestJobVerifyCmdRegistered(t *testing.T) { for _, c := range jobCmd.Commands() { if c.Name() == "verify" { return } } t.Fatal("job verify command not registered on jobCmd") } func TestJobVerifyValidSpec(t *testing.T) { setupVerifyEnv(t) mt := &mockVerifyTransport{execOut: []byte("verified\n")} jobVerifyTransportOverride = mt defer func() { jobVerifyTransportOverride = nil }() resetRootFlags(t) spec := writeMDSpec(t, validJobMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "verify", spec, "--lead", "lead:22"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job verify valid: %v", err) } out := buf.String() if !strings.Contains(out, "Planned allocations") { t.Errorf("expected planned allocs in output: %s", out) } if !strings.Contains(out, "systemd") && !strings.Contains(out, "unit") { t.Errorf("expected systemd unit in planned files: %s", out) } if len(mt.writes) == 0 { t.Errorf("expected bundle to be staged (writes), got 0") } if len(mt.execs) != 1 { t.Errorf("expected 1 exec (verify.sh), got %d", len(mt.execs)) } } func TestJobVerifyInvalidSpecFails(t *testing.T) { setupVerifyEnv(t) mt := &mockVerifyTransport{} jobVerifyTransportOverride = mt defer func() { jobVerifyTransportOverride = nil }() resetRootFlags(t) spec := writeMDSpec(t, serviceMissingPortsMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "verify", spec, "--lead", "lead:22"}) if err := rootCmd.Execute(); err == nil { t.Fatal("expected error for invalid spec, got nil") } if len(mt.writes) != 0 { t.Errorf("should not stage bundle for invalid spec, got %d writes", len(mt.writes)) } } func TestJobVerifyJSONOutput(t *testing.T) { setupVerifyEnv(t) mt := &mockVerifyTransport{execOut: []byte("verified\n")} jobVerifyTransportOverride = mt defer func() { jobVerifyTransportOverride = nil }() resetRootFlags(t) spec := writeMDSpec(t, validJobMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "verify", spec, "--lead", "lead:22", "--json"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job verify --json: %v", err) } var report verifyReport if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &report); err != nil { t.Fatalf("unmarshal verify json: %v\n%s", err, buf.String()) } if report.Name != "my-job" { t.Errorf("report.Name = %q, want my-job", report.Name) } if len(report.PlannedFiles) == 0 { t.Errorf("expected planned files, got 0") } if report.TxnID == "" { t.Errorf("expected txn id, got empty") } } func TestJobVerifyNamespaceScoping(t *testing.T) { setupVerifyEnv(t) mt := &mockVerifyTransport{execOut: []byte("verified\n")} jobVerifyTransportOverride = mt defer func() { jobVerifyTransportOverride = nil }() resetRootFlags(t) spec := writeMDSpec(t, validJobMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "verify", spec, "--lead", "lead:22", "--namespace", "prod"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job verify --namespace: %v", err) } out := buf.String() if !strings.Contains(out, "Namespace: prod") { t.Errorf("expected namespace in output: %s", out) } } func TestJobVerifyNoSideEffects(t *testing.T) { setupVerifyEnv(t) mt := &mockVerifyTransport{execOut: []byte("verified\n")} jobVerifyTransportOverride = mt defer func() { jobVerifyTransportOverride = nil }() resetRootFlags(t) spec := writeMDSpec(t, validJobMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "verify", spec, "--lead", "lead:22"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job verify: %v", err) } // Staging writes the bundle files but apply is NOT run. for _, w := range mt.writes { if strings.Contains(w.path, ".applied") { t.Errorf("verify staged a .applied marker (side effect): %s", w.path) } } for _, c := range mt.execs { if strings.Contains(c, "apply") { t.Errorf("verify ran apply (side effect): %s", c) } if strings.Contains(c, "orca-pull.sh") { t.Errorf("verify ran orca-pull.sh (side effect): %s", c) } } } func TestJobVerifyPreflightDriftReported(t *testing.T) { setupVerifyEnv(t) mt := &mockVerifyTransport{execOut: []byte("verify: drift /etc/systemd/system/orca-v1-my-job.service has drifted from last apply\n")} jobVerifyTransportOverride = mt defer func() { jobVerifyTransportOverride = nil }() resetRootFlags(t) spec := writeMDSpec(t, validJobMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "verify", spec, "--lead", "lead:22"}) // verify.sh exit 1 from drift is treated as a verify-failure by // the mock (execErr). But here execOut is set and execErr is nil, // so verify "passes" and drift is reported. We assert drift is // surfaced in the output and does NOT fail the dry-run. if err := rootCmd.Execute(); err != nil { t.Fatalf("job verify with drift should not fail dry-run: %v", err) } out := buf.String() if !strings.Contains(strings.ToLower(out), "drift") { t.Errorf("expected drift in output: %s", out) } } func TestJobVerifyMissingLeadFails(t *testing.T) { setupVerifyEnv(t) resetRootFlags(t) spec := writeMDSpec(t, validJobMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "verify", spec}) if err := rootCmd.Execute(); err == nil { t.Fatal("expected error for missing --lead, got nil") } } func TestJobVerifyMissingMasterKeyFails(t *testing.T) { t.Setenv("ORCA_HOME", t.TempDir()) resetRootFlags(t) spec := writeMDSpec(t, validJobMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "verify", spec, "--lead", "lead:22"}) if err := rootCmd.Execute(); err == nil { t.Fatal("expected error for missing master key, got nil") } }