package stepca import ( "context" "errors" "os" "path/filepath" "strings" "testing" "git.cloudinit.dev/coreci/orca/internal/paths" "git.cloudinit.dev/coreci/orca/internal/sshpush" ) // mockExec is a record-and-replay execer for the stepca.Client. It // stores every command it received keyed by a substring match, so a // test can assert "Init ran `step ca init`" without coupling to // exact-flag ordering. Each entry maps a substring the test expects // to appear in the command to the output that should be returned. type mockExec struct { // responses is a list of (substring, output, err). The first // matching entry wins; an entry with an empty substring matches // any command (catch-all). responses []mockResp // calls records every command the client issued, in order. calls []string } type mockResp struct { match string out []byte err error } func (m *mockExec) Exec(ctx context.Context, peer string, cmd string) ([]byte, error) { m.calls = append(m.calls, cmd) for _, r := range m.responses { if r.match == "" || strings.Contains(cmd, r.match) { return r.out, r.err } } return nil, nil } // newMockClient returns a Client wired to a mockExec and an ORCA_HOME // under a temp dir (so paths.CACertPath() resolves to a writable path // during Init tests). func newMockClient(t *testing.T, lead string) (*Client, *mockExec) { t.Helper() dir := t.TempDir() t.Setenv("ORCA_HOME", dir) mx := &mockExec{} c := NewClient(nil, lead) c.exec = mx return c, mx } func containsCall(t *testing.T, mx *mockExec, want string) { t.Helper() for _, c := range mx.calls { if strings.Contains(c, want) { return } } t.Errorf("no exec call contained %q; calls were:\n%s", want, strings.Join(mx.calls, "\n")) } func TestNewClient_Defaults(t *testing.T) { tr := sshpush.NewTransport("/tmp/key", "/tmp/kh") c := NewClient(tr, "lead:22") if c.leadPeer != "lead:22" { t.Errorf("leadPeer = %q", c.leadPeer) } if c.transport != tr { t.Error("transport not stored") } if c.exec == nil { t.Error("exec seam is nil") } } func TestClient_Preflight_LeadUnset(t *testing.T) { c, _ := newMockClient(t, "") if err := c.Init(context.Background(), "n", "d", "a"); !errors.Is(err, ErrLeadUnset) { t.Errorf("Init with empty lead: err = %v, want ErrLeadUnset", err) } if _, _, err := c.IssueServerCert(context.Background(), "p", nil); !errors.Is(err, ErrLeadUnset) { t.Errorf("IssueServerCert: err = %v, want ErrLeadUnset", err) } if _, _, err := c.IssueSVID(context.Background(), "spiffe://orca/x", nil); !errors.Is(err, ErrLeadUnset) { t.Errorf("IssueSVID: err = %v, want ErrLeadUnset", err) } if err := c.RenewServerCert(context.Background(), "p"); !errors.Is(err, ErrLeadUnset) { t.Errorf("RenewServerCert: err = %v, want ErrLeadUnset", err) } if _, err := c.Fingerprint(context.Background()); !errors.Is(err, ErrLeadUnset) { t.Errorf("Fingerprint: err = %v, want ErrLeadUnset", err) } } func TestClient_Preflight_NilExec(t *testing.T) { c := &Client{leadPeer: "lead:22"} // exec is nil if err := c.Init(context.Background(), "n", "d", "a"); err == nil { t.Fatal("Init with nil exec: expected error, got nil") } } func TestInit_Success(t *testing.T) { c, mx := newMockClient(t, "lead:22") caPEM := []byte("-----BEGIN CERTIFICATE-----\nFAKE\n-----END CERTIFICATE-----\n") mx.responses = []mockResp{ {match: "step ca init", out: nil, err: nil}, {match: "cat '/etc/step-ca/certs/root_ca.crt'", out: caPEM, err: nil}, } if err := c.Init(context.Background(), "orca", "ca.orca.local", ":8443"); err != nil { t.Fatalf("Init: %v", err) } containsCall(t, mx, "step ca init --name 'orca'") containsCall(t, mx, "--dns 'ca.orca.local'") containsCall(t, mx, "--address ':8443'") containsCall(t, mx, "--provisioner orca-oidc") containsCall(t, mx, "--deployment-type standalone") // Root CA mirrored to paths.CACertPath(). got, err := os.ReadFile(paths.CACertPath()) if err != nil { t.Fatalf("read mirrored CA: %v", err) } if string(got) != string(caPEM) { t.Errorf("mirrored CA = %q, want %q", got, caPEM) } } func TestInit_StepCLIFails(t *testing.T) { c, mx := newMockClient(t, "lead:22") stepErr := errors.New("step: non-zero exit 1") mx.responses = []mockResp{ {match: "step ca init", out: nil, err: stepErr}, } err := c.Init(context.Background(), "orca", "ca.orca.local", ":8443") if err == nil { t.Fatal("Init: expected error, got nil") } if !strings.Contains(err.Error(), "stepca: init") { t.Errorf("err = %v, want wrapped 'stepca: init'", err) } } func TestInit_EmptyRootCA(t *testing.T) { c, mx := newMockClient(t, "lead:22") mx.responses = []mockResp{ {match: "step ca init", out: nil, err: nil}, {match: "cat '/etc/step-ca/certs/root_ca.crt'", out: nil, err: nil}, } err := c.Init(context.Background(), "orca", "ca.orca.local", ":8443") if err == nil { t.Fatal("Init with empty root CA: expected error, got nil") } if !errors.Is(err, ErrStepCLI) { t.Errorf("err = %v, want ErrStepCLI", err) } } func TestIssueServerCert_Success(t *testing.T) { c, mx := newMockClient(t, "lead:22") certPEM := []byte("SERVER-CERT-PEM") keyPEM := []byte("SERVER-KEY-PEM") mx.responses = []mockResp{ {match: "step ca certificate", out: nil, err: nil}, {match: "cat '/etc/orca/step-tmp/orca-peer1.crt'", out: certPEM, err: nil}, {match: "cat '/etc/orca/step-tmp/orca-peer1.key'", out: keyPEM, err: nil}, {match: "rm -f", out: nil, err: nil}, } gotCert, gotKey, err := c.IssueServerCert(context.Background(), "peer1", []string{"peer1.orca.local", "10.0.0.1"}) if err != nil { t.Fatalf("IssueServerCert: %v", err) } if gotCert != string(certPEM) { t.Errorf("cert = %q", gotCert) } if gotKey != string(keyPEM) { t.Errorf("key = %q", gotKey) } containsCall(t, mx, "step ca certificate 'peer1'") containsCall(t, mx, "--not-after '2160h'") containsCall(t, mx, "--san 'peer1.orca.local'") containsCall(t, mx, "--san '10.0.0.1'") // Server cert path must NOT pin a provisioner (uses default). for _, call := range mx.calls { if strings.HasPrefix(call, "step ca certificate") && strings.Contains(call, "--provisioner") { t.Errorf("server cert should not pin provisioner; cmd: %s", call) } } } func TestIssueSVID_Success(t *testing.T) { c, mx := newMockClient(t, "lead:22") spiffe := "spiffe://orca/ns/_defaults/job/web/alloc/0" certPEM := []byte("SVID-CERT-PEM") keyPEM := []byte("SVID-KEY-PEM") mx.responses = []mockResp{ {match: "step ca certificate", out: nil, err: nil}, {match: "cat '/etc/orca/step-tmp/orca-spiffe-orca_ns__defaults_job_web_alloc_0.crt'", out: certPEM, err: nil}, {match: "cat '/etc/orca/step-tmp/orca-spiffe-orca_ns__defaults_job_web_alloc_0.key'", out: keyPEM, err: nil}, {match: "rm -f", out: nil, err: nil}, } gotCert, gotKey, err := c.IssueSVID(context.Background(), spiffe, []string{"web.orca.local"}) if err != nil { t.Fatalf("IssueSVID: %v", err) } if gotCert != string(certPEM) || gotKey != string(keyPEM) { t.Errorf("cert/key mismatch") } containsCall(t, mx, "step ca certificate") containsCall(t, mx, "--not-after '24h'") containsCall(t, mx, "--provisioner 'orca-oidc'") // SPIFFE ID is both the subject AND a SAN. containsCall(t, mx, "--san '"+spiffe+"'") } func TestIssueServerCert_StepFails(t *testing.T) { c, mx := newMockClient(t, "lead:22") mx.responses = []mockResp{ {match: "step ca certificate", out: nil, err: errors.New("step: exit 1")}, } _, _, err := c.IssueServerCert(context.Background(), "peer1", nil) if err == nil || !strings.Contains(err.Error(), "stepca: issue") { t.Errorf("err = %v, want wrapped 'stepca: issue'", err) } } func TestIssueServerCert_ReadCertFails(t *testing.T) { c, mx := newMockClient(t, "lead:22") mx.responses = []mockResp{ {match: "step ca certificate", out: nil, err: nil}, {match: "cat '/etc/orca/step-tmp/orca-peer1.crt'", out: nil, err: errors.New("ssh: cat failed")}, {match: "cat '/etc/orca/step-tmp/orca-peer1.key'", out: nil, err: nil}, } _, _, err := c.IssueServerCert(context.Background(), "peer1", nil) if err == nil || !strings.Contains(err.Error(), "read") { t.Errorf("err = %v, want wrapped 'read'", err) } } func TestIssueServerCert_EmptyCert(t *testing.T) { c, mx := newMockClient(t, "lead:22") mx.responses = []mockResp{ {match: "step ca certificate", out: nil, err: nil}, {match: "cat '/etc/orca/step-tmp/orca-peer1.crt'", out: nil, err: nil}, {match: "cat '/etc/orca/step-tmp/orca-peer1.key'", out: []byte("KEY"), err: nil}, {match: "rm -f", out: nil, err: nil}, } _, _, err := c.IssueServerCert(context.Background(), "peer1", nil) if err == nil || !errors.Is(err, ErrStepCLI) { t.Errorf("err = %v, want ErrStepCLI", err) } } func TestRenewServerCert_Success(t *testing.T) { c, mx := newMockClient(t, "lead:22") mx.responses = []mockResp{ {match: "step ca renew", out: nil, err: nil}, } if err := c.RenewServerCert(context.Background(), "peer1"); err != nil { t.Fatalf("RenewServerCert: %v", err) } containsCall(t, mx, "step ca renew '/etc/orca/step-tmp/orca-peer1.crt' '/etc/orca/step-tmp/orca-peer1.key' --force") } func TestRenewServerCert_Fails(t *testing.T) { c, mx := newMockClient(t, "lead:22") mx.responses = []mockResp{ {match: "step ca renew", out: nil, err: errors.New("step: renew failed")}, } err := c.RenewServerCert(context.Background(), "peer1") if err == nil || !strings.Contains(err.Error(), "stepca: renew") { t.Errorf("err = %v, want wrapped 'stepca: renew'", err) } } func TestFingerprint_Success(t *testing.T) { c, mx := newMockClient(t, "lead:22") mx.responses = []mockResp{ {match: "step certificate fingerprint", out: []byte("a1b2c3d4e5f6\n"), err: nil}, } fp, err := c.Fingerprint(context.Background()) if err != nil { t.Fatalf("Fingerprint: %v", err) } if fp != "a1b2c3d4e5f6" { t.Errorf("fp = %q, want a1b2c3d4e5f6 (trimmed)", fp) } containsCall(t, mx, "step certificate fingerprint '/etc/step-ca/certs/root_ca.crt'") } func TestFingerprint_Empty(t *testing.T) { c, mx := newMockClient(t, "lead:22") mx.responses = []mockResp{ {match: "step certificate fingerprint", out: []byte(""), err: nil}, } _, err := c.Fingerprint(context.Background()) if err == nil || !errors.Is(err, ErrStepCLI) { t.Errorf("err = %v, want ErrStepCLI", err) } } func TestFingerprint_Fails(t *testing.T) { c, mx := newMockClient(t, "lead:22") mx.responses = []mockResp{ {match: "step certificate fingerprint", out: nil, err: errors.New("ssh: exec failed")}, } _, err := c.Fingerprint(context.Background()) if err == nil || !strings.Contains(err.Error(), "stepca: fingerprint") { t.Errorf("err = %v, want wrapped 'stepca: fingerprint'", err) } } func TestInit_MkdirFails(t *testing.T) { // Point ORCA_HOME at a path that cannot be created under to // force MkdirAll failure. We use a file as the parent. dir := t.TempDir() blocker := filepath.Join(dir, "block") if err := os.WriteFile(blocker, []byte("x"), 0o644); err != nil { t.Fatalf("write blocker: %v", err) } t.Setenv("ORCA_HOME", filepath.Join(blocker, "sub")) // Construct the client directly (not newMockClient, which // resets ORCA_HOME to a fresh temp dir). mx := &mockExec{} caPEM := []byte("FAKE") mx.responses = []mockResp{ {match: "step ca init", out: nil, err: nil}, {match: "cat '/etc/step-ca/certs/root_ca.crt'", out: caPEM, err: nil}, } c := NewClient(nil, "lead:22") c.exec = mx err := c.Init(context.Background(), "orca", "ca.orca.local", ":8443") if err == nil { t.Fatal("Init: expected mkdir error, got nil") } if !strings.Contains(err.Error(), "mkdir") { t.Errorf("err = %v, want 'mkdir'", err) } } func TestShellQuote(t *testing.T) { got := shellQuote("a'b") want := "'a'\\''b'" if got != want { t.Errorf("shellQuote = %q, want %q", got, want) } } func TestSanitize(t *testing.T) { cases := []struct{ in, want string }{ {"spiffe://orca/ns/_defaults/job/web/alloc/0", "spiffe-orca_ns__defaults_job_web_alloc_0"}, {"plain-host", "plain-host"}, {"a b", "a_b"}, } for _, tc := range cases { if got := sanitize(tc.in); got != tc.want { t.Errorf("sanitize(%q) = %q, want %q", tc.in, got, tc.want) } } }