package txn import ( "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "sync" "testing" "git.cloudinit.dev/coreci/orca/internal/sshpush" ) func testKey() []byte { return []byte("0123456789abcdef0123456789abcdef") } func mustRender(t *testing.T, desired any) *Bundle { t.Helper() b, err := RenderBundle(desired, testKey()) if err != nil { t.Fatalf("RenderBundle: %v", err) } return b } func TestRenderBundleValidIDAndSignature(t *testing.T) { desired := []map[string]any{ {"path": "/etc/orca/x.conf", "content": "hello", "mode": "0644"}, } b := mustRender(t, desired) if !strings.HasPrefix(string(b.ID), "T-") { t.Fatalf("ID %q missing T- prefix", b.ID) } if len(strings.TrimPrefix(string(b.ID), "T-")) != txnIDHexLen { t.Fatalf("ID %q suffix not %d hex chars", b.ID, txnIDHexLen) } // Deterministic: same input => same ID. b2 := mustRender(t, desired) if b.ID != b2.ID { t.Fatalf("non-deterministic ID: %s vs %s", b.ID, b2.ID) } // Manifest signature verifies. if err := VerifyManifestSignature(b.Manifest, b.ManifestSig, testKey()); err != nil { t.Fatalf("VerifyManifestSignature: %v", err) } // Manifest lists all four bundle files + desired-state. var m Manifest if err := json.Unmarshal(b.Manifest, &m); err != nil { t.Fatalf("unmarshal manifest: %v", err) } if m.TxnID != b.ID { t.Errorf("manifest TxnID %q != bundle ID %q", m.TxnID, b.ID) } names := map[string]bool{} for _, e := range m.Files { names[e.Name] = true if e.SHA256 == "" { t.Errorf("manifest entry %s missing sha256", e.Name) } } for _, want := range []string{fileDesiredState, fileApply, fileVerify, fileRollback} { if !names[want] { t.Errorf("manifest missing file %s", want) } } // Scripts are bash. if !strings.HasPrefix(string(b.ApplyScript), "#!/usr/bin/env bash") { t.Errorf("apply.sh not bash") } if !strings.HasPrefix(string(b.VerifyScript), "#!/usr/bin/env bash") { t.Errorf("verify.sh not bash") } if !strings.HasPrefix(string(b.RollbackScript), "#!/usr/bin/env bash") { t.Errorf("rollback.sh not bash") } } func TestRenderBundleContentAddressed(t *testing.T) { a := mustRender(t, []map[string]any{{"path": "/a"}}) b := mustRender(t, []map[string]any{{"path": "/b"}}) if a.ID == b.ID { t.Fatalf("distinct desired states yielded same TxnID %s", a.ID) } } func TestRenderBundleRejectsEmptyKey(t *testing.T) { _, err := RenderBundle([]string{"x"}, nil) if err == nil { t.Fatal("RenderBundle with empty key should fail") } } func TestVerifyManifestSignatureTampered(t *testing.T) { b := mustRender(t, []map[string]any{{"path": "/x"}}) // Tamper the manifest content. tampered := make([]byte, len(b.Manifest)) copy(tampered, b.Manifest) tampered[0] ^= 0xff err := VerifyManifestSignature(tampered, b.ManifestSig, testKey()) if !errors.Is(err, ErrSignatureMismatch) { t.Fatalf("tampered manifest: got %v, want ErrSignatureMismatch", err) } // Tamper the signature (flip a hex char so the digest changes // but stays valid hex). badSig := make([]byte, len(b.ManifestSig)) copy(badSig, b.ManifestSig) if len(badSig) > 0 { // Flip the first hex char between 0 and 1. if badSig[0] == '0' { badSig[0] = '1' } else { badSig[0] = '0' } } err = VerifyManifestSignature(b.Manifest, badSig, testKey()) if !errors.Is(err, ErrSignatureMismatch) { t.Fatalf("tampered sig: got %v, want ErrSignatureMismatch", err) } // Wrong key. wrongKey := []byte("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") err = VerifyManifestSignature(b.Manifest, b.ManifestSig, wrongKey) if !errors.Is(err, ErrSignatureMismatch) { t.Fatalf("wrong key: got %v, want ErrSignatureMismatch", err) } } // mockTransport records all operations against an in-memory file map. type mockTransport struct { mu sync.Mutex files map[string][]byte execOut map[string][]byte execErr map[string]error writes []string execs []string } func newMockTransport() *mockTransport { return &mockTransport{ files: make(map[string][]byte), execOut: make(map[string][]byte), execErr: make(map[string]error), } } func (m *mockTransport) WriteFileIdempotent(_ context.Context, _ string, path string, content []byte, _ os.FileMode) (bool, error) { m.mu.Lock() defer m.mu.Unlock() m.writes = append(m.writes, path) if existing, ok := m.files[path]; ok && string(existing) == string(content) { return false, nil } cp := make([]byte, len(content)) copy(cp, content) m.files[path] = cp return true, nil } func (m *mockTransport) Exec(_ context.Context, _ string, cmd string) ([]byte, error) { m.mu.Lock() defer m.mu.Unlock() m.execs = append(m.execs, cmd) // Match by substring key in execOut/execErr maps. for key, out := range m.execOut { if strings.Contains(cmd, key) { if err, ok := m.execErr[key]; ok { return out, err } return out, nil } } for key, err := range m.execErr { if strings.Contains(cmd, key) { return m.execOut[key], err } } return nil, nil } func (m *mockTransport) hasFile(path string) bool { m.mu.Lock() defer m.mu.Unlock() _, ok := m.files[path] return ok } func TestStageWritesBundleFiles(t *testing.T) { b := mustRender(t, []map[string]any{{"path": "/etc/orca/x.conf", "content": "hello"}}) mt := newMockTransport() if err := Stage(b, "lead:22", mt); err != nil { t.Fatalf("Stage: %v", err) } dir := remoteTxnDir(b.ID) for _, name := range []string{fileDesiredState, fileApply, fileVerify, fileRollback, fileManifest, fileManifestSig} { if !mt.hasFile(dir + "/" + name) { t.Errorf("staged file missing: %s", name) } } } func TestStageNilBundle(t *testing.T) { mt := newMockTransport() if err := Stage(nil, "lead:22", mt); err == nil { t.Fatal("Stage(nil) should fail") } } func TestStageNilTransport(t *testing.T) { b := mustRender(t, []map[string]any{{"path": "/x"}}) if err := Stage(b, "lead:22", nil); err == nil { t.Fatal("Stage with nil transport should fail") } } func TestApplyClusterWideSuccess(t *testing.T) { b := mustRender(t, []map[string]any{{"path": "/etc/orca/x.conf", "content": "hello"}}) mt := newMockTransport() if err := Stage(b, "lead:22", mt); err != nil { t.Fatalf("Stage: %v", err) } mt.execOut["orca-pull.sh"] = []byte("applied") err := Apply(context.Background(), b.ID, "lead:22", mt, ApplyOptions{ Force: true, AcknowledgeRisk: true, }) if err != nil { t.Fatalf("Apply: %v", err) } if len(mt.execs) != 1 { t.Fatalf("expected 1 exec, got %d", len(mt.execs)) } cmd := mt.execs[0] if !strings.Contains(cmd, "--force") { t.Errorf("cmd missing --force: %s", cmd) } if !strings.Contains(cmd, "--i-understand-the-risk") { t.Errorf("cmd missing --i-understand-the-risk: %s", cmd) } if strings.Contains(cmd, "--namespace") { t.Errorf("cluster-wide cmd should not have --namespace: %s", cmd) } } func TestApplyClusterWideYes(t *testing.T) { b := mustRender(t, []map[string]any{{"path": "/x"}}) mt := newMockTransport() _ = Stage(b, "lead:22", mt) mt.execOut["orca-pull.sh"] = []byte("applied") err := Apply(context.Background(), b.ID, "lead:22", mt, ApplyOptions{ Force: true, Yes: true, }) if err != nil { t.Fatalf("Apply --yes: %v", err) } if !strings.Contains(mt.execs[0], "--yes") { t.Errorf("cmd missing --yes: %s", mt.execs[0]) } } func TestApplyNamespaceScopedSuccess(t *testing.T) { b := mustRender(t, []map[string]any{{"path": "/x"}}) mt := newMockTransport() _ = Stage(b, "lead:22", mt) mt.execOut["orca-pull.sh"] = []byte("applied") err := Apply(context.Background(), b.ID, "lead:22", mt, ApplyOptions{ Namespace: "default", }) if err != nil { t.Fatalf("Apply ns-scoped: %v", err) } cmd := mt.execs[0] if !strings.Contains(cmd, "--namespace") { t.Errorf("ns-scoped cmd missing --namespace: %s", cmd) } if strings.Contains(cmd, "--force") { t.Errorf("ns-scoped cmd should not have --force: %s", cmd) } } func TestApplyClusterWideRefusesWithoutForce(t *testing.T) { b := mustRender(t, []map[string]any{{"path": "/x"}}) mt := newMockTransport() _ = Stage(b, "lead:22", mt) err := Apply(context.Background(), b.ID, "lead:22", mt, ApplyOptions{}) if !errors.Is(err, ErrClusterWideRequiresForce) { t.Fatalf("expected ErrClusterWideRequiresForce, got %v", err) } if len(mt.execs) != 0 { t.Errorf("should not exec when --force missing") } } func TestApplyClusterWideRefusesWithoutAck(t *testing.T) { b := mustRender(t, []map[string]any{{"path": "/x"}}) mt := newMockTransport() _ = Stage(b, "lead:22", mt) err := Apply(context.Background(), b.ID, "lead:22", mt, ApplyOptions{Force: true}) if !errors.Is(err, ErrClusterWideRequiresAck) { t.Fatalf("expected ErrClusterWideRequiresAck, got %v", err) } } func TestApplyIdempotentNoOp(t *testing.T) { b := mustRender(t, []map[string]any{{"path": "/x"}}) mt := newMockTransport() _ = Stage(b, "lead:22", mt) // Lead returns exit 5 = already-applied no-op. mt.execErr["orca-pull.sh"] = fmt.Errorf("%w: exit 5", sshpush.ErrPermanent) mt.execOut["orca-pull.sh"] = []byte("already-applied") err := Apply(context.Background(), b.ID, "lead:22", mt, ApplyOptions{ Force: true, AcknowledgeRisk: true, }) if !errors.Is(err, ErrAlreadyApplied) { t.Fatalf("expected ErrAlreadyApplied, got %v", err) } } func TestApplyFailureReturnsError(t *testing.T) { b := mustRender(t, []map[string]any{{"path": "/x"}}) mt := newMockTransport() _ = Stage(b, "lead:22", mt) mt.execErr["orca-pull.sh"] = fmt.Errorf("%w: exit 1", sshpush.ErrPermanent) mt.execOut["orca-pull.sh"] = []byte("apply failure") err := Apply(context.Background(), b.ID, "lead:22", mt, ApplyOptions{ Force: true, AcknowledgeRisk: true, }) if err == nil { t.Fatal("Apply should fail on exit 1") } if errors.Is(err, ErrAlreadyApplied) { t.Fatal("exit 1 should not be treated as already-applied") } } func TestApplyNilTransport(t *testing.T) { b := mustRender(t, []map[string]any{{"path": "/x"}}) err := Apply(context.Background(), b.ID, "lead:22", nil, ApplyOptions{ Force: true, AcknowledgeRisk: true, }) if err == nil { t.Fatal("Apply with nil transport should fail") } } func TestSignManifestDeterministic(t *testing.T) { manifest := []byte(`{"txn_id":"T-abc"}`) sig := signManifest(manifest, testKey()) mac := hmac.New(sha256.New, testKey()) mac.Write(manifest) want := hex.EncodeToString(mac.Sum(nil)) if string(sig) != want { t.Fatalf("signManifest: got %q want %q", sig, want) } } func TestComputeTxnIDStable(t *testing.T) { data := []byte(`{"x":1}`) id, err := computeTxnID(data) if err != nil { t.Fatalf("computeTxnID: %v", err) } sum := sha256.Sum256(data) h := hex.EncodeToString(sum[:]) want := TxnID("T-" + h[:txnIDHexLen]) if id != want { t.Fatalf("computeTxnID: got %q want %q", id, want) } } // --- REQ-121 / F5 txn apply path allowlist tests --- // TestApplyScriptRejectsDisallowedPath verifies the generated apply.sh // refuses to write paths outside the allowlist. We render a bundle // with a disallowed path, extract the apply.sh, run it with a crafted // desired-state.json, and assert it exits 7 (the refusal code) without // writing the file. func TestApplyScriptRejectsDisallowedPath(t *testing.T) { if testing.Short() { t.Skip("apply.sh exec test skipped in -short mode") } disallowed := []string{ "/etc/shadow", "/root/.ssh/authorized_keys", "/etc/passwd", "/tmp/pwned", "/etc/orca/../../shadow", "relative/path", } for _, p := range disallowed { t.Run(p, func(t *testing.T) { b := mustRender(t, []map[string]any{{"path": p, "content": "pwned"}}) // Write apply.sh + desired-state.json to a temp dir. dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, fileDesiredState), b.DesiredState, 0o600); err != nil { t.Fatalf("write desired-state: %v", err) } if err := os.WriteFile(filepath.Join(dir, fileApply), b.ApplyScript, 0o755); err != nil { t.Fatalf("write apply.sh: %v", err) } cmd := exec.Command("bash", filepath.Join(dir, fileApply)) out, err := cmd.CombinedOutput() if err == nil { t.Fatalf("apply.sh should fail for path %s, got success; output: %s", p, out) } if !strings.Contains(string(out), "refusing to write disallowed path") { t.Errorf("apply.sh output should mention refusal: %s", out) } }) } } // TestApplyScriptAllowsOrcaPaths verifies the allowed prefixes work. func TestApplyScriptAllowsOrcaPaths(t *testing.T) { if testing.Short() { t.Skip("apply.sh exec test skipped in -short mode") } // We can't actually write to /etc/ in a test, so we verify the // allowlist logic in the generated script by checking the script // content contains the allowlist and the path_allowed function. b := mustRender(t, []map[string]any{{"path": "/etc/orca/test"}}) script := string(b.ApplyScript) if !strings.Contains(script, "ALLOWED_PREFIXES") { t.Error("apply.sh missing ALLOWED_PREFIXES") } if !strings.Contains(script, "path_allowed") { t.Error("apply.sh missing path_allowed function") } for _, prefix := range []string{ "/etc/orca/", "/etc/traefik/orca", "/etc/systemd/system/orca-", "/etc/nftables.d/orca", "/etc/syncthing/orca", } { if !strings.Contains(script, prefix) { t.Errorf("apply.sh missing allowed prefix %s", prefix) } } }