diff --git a/go.mod b/go.mod index 0bd9b6e..7fe6c50 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,14 @@ module github.com/temporalio/cloud-cli -go 1.26.8 +go 1.27 require ( github.com/aws/aws-sdk-go-v2 v1.41.11 github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.19.0 + github.com/google/go-cmp v0.7.0 github.com/kylelemons/godebug v1.1.0 + github.com/mattn/go-isatty v0.0.22 github.com/olekukonko/tablewriter v1.1.4 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/spf13/cobra v1.10.2 @@ -20,6 +22,7 @@ require ( golang.org/x/oauth2 v0.36.0 golang.org/x/term v0.43.0 google.golang.org/grpc v1.82.1 + google.golang.org/protobuf v1.36.11 ) require ( @@ -28,6 +31,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -48,6 +52,8 @@ require ( github.com/knadh/koanf/providers/posflag v1.0.1 // indirect github.com/knadh/koanf/providers/structs v1.0.0 // indirect github.com/knadh/koanf/v2 v2.3.2 // indirect + github.com/mattn/go-colorable v0.1.15 // indirect + github.com/mattn/go-runewidth v0.0.24 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/nexus-rpc/nexus-proto-annotations v0.1.0 // indirect @@ -55,6 +61,7 @@ require ( github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.3.0 // indirect github.com/olekukonko/ll v0.1.8 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/robfig/cron v1.2.0 // indirect github.com/rs/zerolog v1.34.0 // indirect github.com/stretchr/objx v0.5.3 // indirect @@ -67,6 +74,7 @@ require ( golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.44.0 // indirect @@ -75,14 +83,4 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -require ( - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/mattn/go-colorable v0.1.15 // indirect - github.com/mattn/go-isatty v0.0.22 - github.com/mattn/go-runewidth v0.0.24 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - golang.org/x/sys v0.45.0 // indirect - google.golang.org/protobuf v1.36.11 -) - tool github.com/vektra/mockery/v3 diff --git a/go.sum b/go.sum index fa4872c..091d97a 100644 --- a/go.sum +++ b/go.sum @@ -154,8 +154,6 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.temporal.io/api v1.63.6-0.20260811224032-89570b10e9e2 h1:VeNTxlOHETym01zX6kSXHIVw4nc2RrX/KVgepQ/dr/8= go.temporal.io/api v1.63.6-0.20260811224032-89570b10e9e2/go.mod h1:SrlW2JMwVlDP4nRWSNznUFqnSHd+YeMDS1BkYo63HCQ= -go.temporal.io/cloud-sdk v0.18.0 h1:7jGf76PwO6teN/wCkFlrf5yEsJcUrtbMw1hxV/l0a9M= -go.temporal.io/cloud-sdk v0.18.0/go.mod h1:W2O9t9tvo3Q/LhGgYdj8JijWbN5C84os+cz/BadIHYI= go.temporal.io/cloud-sdk v0.19.0 h1:Vjr8a75hdxbq6VAiZJz7WboqhGA2NhmXl5A6ZFrVt60= go.temporal.io/cloud-sdk v0.19.0/go.mod h1:W2O9t9tvo3Q/LhGgYdj8JijWbN5C84os+cz/BadIHYI= go.temporal.io/sdk v1.44.1 h1:Mt2OZLZpqkzDIdg9YyQzO0Rb/HqCDnnqHlIAGAJ5gqM= diff --git a/temporalcloudcli/commands.account.auditlog.sink.kinesis_test.go b/temporalcloudcli/commands.account.auditlog.sink.kinesis_test.go index 883468f..0096019 100644 --- a/temporalcloudcli/commands.account.auditlog.sink.kinesis_test.go +++ b/temporalcloudcli/commands.account.auditlog.sink.kinesis_test.go @@ -3,7 +3,6 @@ package temporalcloudcli_test import ( "bytes" "context" - "encoding/json" "errors" "testing" @@ -440,7 +439,7 @@ func TestValidateAuditLogSinkKinesis_Success(t *testing.T) { var out struct { Status string `json:"status"` } - require.NoError(t, json.Unmarshal(buf.Bytes(), &out)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &out)) assert.Equal(t, "valid", out.Status) } diff --git a/temporalcloudcli/commands.account.auditlog.sink.pubsub_test.go b/temporalcloudcli/commands.account.auditlog.sink.pubsub_test.go index b1204e1..77647d0 100644 --- a/temporalcloudcli/commands.account.auditlog.sink.pubsub_test.go +++ b/temporalcloudcli/commands.account.auditlog.sink.pubsub_test.go @@ -3,7 +3,6 @@ package temporalcloudcli_test import ( "bytes" "context" - "encoding/json" "errors" "testing" @@ -329,9 +328,13 @@ func TestValidateAuditLogSinkPubSub_Success(t *testing.T) { }) require.NoError(t, err) - var out struct{ Status string } - require.NoError(t, json.Unmarshal(buf.Bytes(), &out)) - assert.Equal(t, struct{ Status string }{Status: "valid"}, out) + var out struct { + Status string `json:"status"` + } + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &out)) + assert.Equal(t, struct { + Status string `json:"status"` + }{Status: "valid"}, out) } // TestValidateAuditLogSinkPubSub_APIError verifies that a ValidateAccountAuditLogSink error is returned. diff --git a/temporalcloudcli/commands.account.auditlog.sink_test.go b/temporalcloudcli/commands.account.auditlog.sink_test.go index bf640e5..fe10529 100644 --- a/temporalcloudcli/commands.account.auditlog.sink_test.go +++ b/temporalcloudcli/commands.account.auditlog.sink_test.go @@ -3,10 +3,10 @@ package temporalcloudcli_test import ( "bytes" "context" - "encoding/json" "errors" "testing" + "github.com/google/go-cmp/cmp" cloudmock "github.com/temporalio/cloud-cli/internal/cloudservice/mock" "github.com/temporalio/cloud-cli/temporalcloudcli" "github.com/temporalio/cloud-cli/temporalcloudcli/internal/printer" @@ -14,7 +14,7 @@ import ( accountv1 "go.temporal.io/cloud-sdk/api/account/v1" cloudservice "go.temporal.io/cloud-sdk/api/cloudservice/v1" operation "go.temporal.io/cloud-sdk/api/operation/v1" - "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/testing/protocmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -44,7 +44,7 @@ func TestGetAuditLogSink_Success(t *testing.T) { // Printer uses protojson for proto messages (camelCase output). var out accountv1.AuditLogSink - require.NoError(t, protojson.Unmarshal(buf.Bytes(), &out)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &out)) assert.Equal(t, sink.Name, out.Name) assert.Equal(t, sink.ResourceVersion, out.ResourceVersion) } @@ -91,15 +91,16 @@ func TestListAuditLogSinks_Success(t *testing.T) { require.NoError(t, err) type listResponse struct { - Sinks []*accountv1.AuditLogSink `json:"sinks"` - NextPageToken string `json:"nextPageToken"` + Sinks []*accountv1.AuditLogSink + NextPageToken string } var out listResponse - require.NoError(t, json.Unmarshal(buf.Bytes(), &out)) - assert.Equal(t, listResponse{ + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &out)) + assert.Empty(t, cmp.Diff(listResponse{ Sinks: sinks, NextPageToken: "next-token", - }, out) + }, out, + protocmp.Transform())) } func TestListAuditLogSinks_WithPagination(t *testing.T) { diff --git a/temporalcloudcli/commands.account.auditlog_test.go b/temporalcloudcli/commands.account.auditlog_test.go index b6d840d..23633a9 100644 --- a/temporalcloudcli/commands.account.auditlog_test.go +++ b/temporalcloudcli/commands.account.auditlog_test.go @@ -3,16 +3,17 @@ package temporalcloudcli_test import ( "bytes" "context" - "encoding/json" "errors" "testing" "time" + "github.com/google/go-cmp/cmp" cloudmock "github.com/temporalio/cloud-cli/internal/cloudservice/mock" "github.com/temporalio/cloud-cli/temporalcloudcli" "github.com/temporalio/cloud-cli/temporalcloudcli/internal/printer" auditlogv1 "go.temporal.io/cloud-sdk/api/auditlog/v1" cloudservice "go.temporal.io/cloud-sdk/api/cloudservice/v1" + "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/timestamppb" "github.com/stretchr/testify/assert" @@ -23,8 +24,8 @@ func TestListAuditLogs_Success(t *testing.T) { mockCloud := cloudmock.NewMockCloudServiceClient(t) expected := struct { - AuditLogs []*auditlogv1.LogRecord `json:"auditLogs"` - NextPageToken string `json:"nextPageToken"` + AuditLogs []*auditlogv1.LogRecord + NextPageToken string }{ AuditLogs: []*auditlogv1.LogRecord{ {LogId: "log-1", Operation: "CreateNamespace"}, @@ -47,11 +48,11 @@ func TestListAuditLogs_Success(t *testing.T) { require.NoError(t, err) var out struct { - AuditLogs []*auditlogv1.LogRecord `json:"auditLogs"` - NextPageToken string `json:"nextPageToken"` + AuditLogs []*auditlogv1.LogRecord + NextPageToken string } - require.NoError(t, json.Unmarshal(buf.Bytes(), &out)) - assert.Equal(t, expected, out) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &out)) + assert.Empty(t, cmp.Diff(expected, out, protocmp.Transform())) } func TestGetAuditLogs_WithPagination(t *testing.T) { diff --git a/temporalcloudcli/commands.connectivity_test.go b/temporalcloudcli/commands.connectivity_test.go index 87e3851..a93b046 100644 --- a/temporalcloudcli/commands.connectivity_test.go +++ b/temporalcloudcli/commands.connectivity_test.go @@ -3,16 +3,16 @@ package temporalcloudcli_test import ( "bytes" "context" - "encoding/json" "errors" "testing" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" cloudservice "go.temporal.io/cloud-sdk/api/cloudservice/v1" connectivityrulev1 "go.temporal.io/cloud-sdk/api/connectivityrule/v1" operation "go.temporal.io/cloud-sdk/api/operation/v1" - "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" cloudmock "github.com/temporalio/cloud-cli/internal/cloudservice/mock" "github.com/temporalio/cloud-cli/temporalcloudcli" @@ -42,12 +42,12 @@ func TestListConnectivityRules_Success(t *testing.T) { require.NoError(t, err) type listConnectivityRulesOutput struct { - ConnectivityRules []*connectivityrulev1.ConnectivityRule `json:"connectivityRules"` - NextPageToken string `json:"nextPageToken"` + ConnectivityRules []*connectivityrulev1.ConnectivityRule + NextPageToken string } var out listConnectivityRulesOutput - require.NoError(t, json.Unmarshal(buf.Bytes(), &out)) - assert.Equal(t, listConnectivityRulesOutput{ConnectivityRules: rules, NextPageToken: ""}, out) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &out)) + assert.Empty(t, cmp.Diff(listConnectivityRulesOutput{ConnectivityRules: rules, NextPageToken: ""}, out, protocmp.Transform())) } // TestListConnectivityRules_WithNamespace verifies that the namespace filter is passed through. @@ -135,8 +135,8 @@ func TestGetConnectivityRule_Success(t *testing.T) { require.NoError(t, err) var out connectivityrulev1.ConnectivityRule - require.NoError(t, json.Unmarshal(buf.Bytes(), &out)) - assert.True(t, proto.Equal(&connectivityrulev1.ConnectivityRule{Id: "rule-1"}, &out)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &out)) + assert.Empty(t, cmp.Diff(&connectivityrulev1.ConnectivityRule{Id: "rule-1", ResourceVersion: "rv-1"}, &out, protocmp.Transform())) } // TestGetConnectivityRule_Error verifies that an API error propagates. diff --git a/temporalcloudcli/commands.namespace.cert_ca_test.go b/temporalcloudcli/commands.namespace.cert_ca_test.go index 4b11463..033a659 100644 --- a/temporalcloudcli/commands.namespace.cert_ca_test.go +++ b/temporalcloudcli/commands.namespace.cert_ca_test.go @@ -8,7 +8,6 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/base64" - "encoding/json" "encoding/pem" "errors" "math/big" @@ -16,6 +15,7 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -27,6 +27,7 @@ import ( "go.temporal.io/cloud-sdk/api/operation/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/testing/protocmp" ) func TestCloudNamespaceCertCaCreateCommand_Success(t *testing.T) { @@ -80,7 +81,7 @@ func TestCloudNamespaceCertCaCreateCommand_Success(t *testing.T) { }, assertResult: func(t *testing.T, buf bytes.Buffer) { var result temporalcloudcli.MutationResult - err := json.Unmarshal(buf.Bytes(), &result) + err := unmarshalWithProtos(buf.Bytes(), &result) require.NoError(t, err) expected := temporalcloudcli.MutationResult{ AsyncOp: &operation.AsyncOperation{ @@ -88,7 +89,7 @@ func TestCloudNamespaceCertCaCreateCommand_Success(t *testing.T) { }, ID: "test-namespace.test-account", } - assert.Equal(t, expected, result) + assert.Empty(t, cmp.Diff(expected, result, protocmp.Transform())) }, }, { @@ -113,7 +114,7 @@ func TestCloudNamespaceCertCaCreateCommand_Success(t *testing.T) { }, assertResult: func(t *testing.T, buf bytes.Buffer) { var result temporalcloudcli.MutationResult - err := json.Unmarshal(buf.Bytes(), &result) + err := unmarshalWithProtos(buf.Bytes(), &result) require.NoError(t, err) expected := temporalcloudcli.MutationResult{ AsyncOp: &operation.AsyncOperation{ @@ -121,7 +122,7 @@ func TestCloudNamespaceCertCaCreateCommand_Success(t *testing.T) { }, ID: "test-namespace.test-account", } - assert.Equal(t, expected, result) + assert.Empty(t, cmp.Diff(expected, result, protocmp.Transform())) }, }, { @@ -313,7 +314,7 @@ func TestCloudNamespaceMtlsCertCaCreateCommand_NothingToChange(t *testing.T) { var result struct { Status string `json:"status"` } - err := json.Unmarshal(buf.Bytes(), &result) + err := unmarshalWithProtos(buf.Bytes(), &result) require.NoError(t, err) assert.Equal(t, "unchanged", result.Status) }, @@ -574,7 +575,7 @@ func TestCloudNamespaceMtlsCertCaDeleteCommand_Success(t *testing.T) { }, assertResult: func(t *testing.T, buf bytes.Buffer) { var result temporalcloudcli.MutationResult - err := json.Unmarshal(buf.Bytes(), &result) + err := unmarshalWithProtos(buf.Bytes(), &result) require.NoError(t, err) expected := temporalcloudcli.MutationResult{ AsyncOp: &operation.AsyncOperation{ @@ -582,7 +583,7 @@ func TestCloudNamespaceMtlsCertCaDeleteCommand_Success(t *testing.T) { }, ID: "test-namespace.test-account", } - assert.Equal(t, expected, result) + assert.Empty(t, cmp.Diff(expected, result, protocmp.Transform())) }, }, { @@ -607,7 +608,7 @@ func TestCloudNamespaceMtlsCertCaDeleteCommand_Success(t *testing.T) { }, assertResult: func(t *testing.T, buf bytes.Buffer) { var result temporalcloudcli.MutationResult - err := json.Unmarshal(buf.Bytes(), &result) + err := unmarshalWithProtos(buf.Bytes(), &result) require.NoError(t, err) expected := temporalcloudcli.MutationResult{ AsyncOp: &operation.AsyncOperation{ @@ -615,7 +616,7 @@ func TestCloudNamespaceMtlsCertCaDeleteCommand_Success(t *testing.T) { }, ID: "test-namespace.test-account", } - assert.Equal(t, expected, result) + assert.Empty(t, cmp.Diff(expected, result, protocmp.Transform())) }, }, { @@ -816,7 +817,7 @@ func TestCloudNamespaceMtlsCertCaDeleteCommand_NothingToChange(t *testing.T) { var result struct { Status string `json:"status"` } - err := json.Unmarshal(buf.Bytes(), &result) + err := unmarshalWithProtos(buf.Bytes(), &result) require.NoError(t, err) assert.Equal(t, "unchanged", result.Status) }, diff --git a/temporalcloudcli/commands.namespace.cert_filter_test.go b/temporalcloudcli/commands.namespace.cert_filter_test.go index 9797d8d..7ef8fe1 100644 --- a/temporalcloudcli/commands.namespace.cert_filter_test.go +++ b/temporalcloudcli/commands.namespace.cert_filter_test.go @@ -3,10 +3,10 @@ package temporalcloudcli_test import ( "bytes" "context" - "encoding/json" "errors" "testing" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -18,6 +18,7 @@ import ( "go.temporal.io/cloud-sdk/api/operation/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/testing/protocmp" ) func TestCloudNamespaceCertFilterListCommand_Success(t *testing.T) { @@ -56,15 +57,15 @@ func TestCloudNamespaceCertFilterListCommand_Success(t *testing.T) { require.NoError(t, capturedErr) var result struct { - CertificateFilters []*namespacev1.CertificateFilterSpec `json:"certificateFilters"` + CertificateFilters []*namespacev1.CertificateFilterSpec } - err := json.Unmarshal(buf.Bytes(), &result) + err := unmarshalWithProtos(buf.Bytes(), &result) require.NoError(t, err) expected := struct { - CertificateFilters []*namespacev1.CertificateFilterSpec `json:"certificateFilters"` + CertificateFilters []*namespacev1.CertificateFilterSpec }{CertificateFilters: expectedFilters} - assert.Equal(t, expected, result) + assert.Empty(t, cmp.Diff(expected, result, protocmp.Transform())) } func TestCloudNamespaceMtlsCertFilterListCommand_EmptyList(t *testing.T) { @@ -93,9 +94,9 @@ func TestCloudNamespaceMtlsCertFilterListCommand_EmptyList(t *testing.T) { require.NoError(t, capturedErr) var result struct { - CertificateFilters []*namespacev1.CertificateFilterSpec `json:"certificateFilters"` + CertificateFilters []*namespacev1.CertificateFilterSpec } - err := json.Unmarshal(buf.Bytes(), &result) + err := unmarshalWithProtos(buf.Bytes(), &result) require.NoError(t, err) assert.Empty(t, result.CertificateFilters) } @@ -187,7 +188,7 @@ func TestCloudNamespaceMtlsCertFilterCreateCommand_Success(t *testing.T) { }, assertResult: func(t *testing.T, buf bytes.Buffer) { var result temporalcloudcli.MutationResult - err := json.Unmarshal(buf.Bytes(), &result) + err := unmarshalWithProtos(buf.Bytes(), &result) require.NoError(t, err) expected := temporalcloudcli.MutationResult{ AsyncOp: &operation.AsyncOperation{ @@ -195,7 +196,7 @@ func TestCloudNamespaceMtlsCertFilterCreateCommand_Success(t *testing.T) { }, ID: "test-namespace.test-account", } - assert.Equal(t, expected, result) + assert.Empty(t, cmp.Diff(expected, result, protocmp.Transform())) }, }, { @@ -352,7 +353,7 @@ func TestCloudNamespaceCertFilterCreateCommand_NothingToChange(t *testing.T) { var result struct { Status string `json:"status"` } - err := json.Unmarshal(buf.Bytes(), &result) + err := unmarshalWithProtos(buf.Bytes(), &result) require.NoError(t, err) expected := struct { Status string `json:"status"` @@ -639,7 +640,7 @@ func TestCloudNamespaceMtlsCertFilterDeleteCommand_Success(t *testing.T) { }, assertResult: func(t *testing.T, buf bytes.Buffer) { var result temporalcloudcli.MutationResult - err := json.Unmarshal(buf.Bytes(), &result) + err := unmarshalWithProtos(buf.Bytes(), &result) require.NoError(t, err) expected := temporalcloudcli.MutationResult{ AsyncOp: &operation.AsyncOperation{ @@ -647,7 +648,7 @@ func TestCloudNamespaceMtlsCertFilterDeleteCommand_Success(t *testing.T) { }, ID: "test-namespace.test-account", } - assert.Equal(t, expected, result) + assert.Empty(t, cmp.Diff(expected, result, protocmp.Transform())) }, }, { @@ -804,7 +805,7 @@ func TestCloudNamespaceMtlsCertFilterDeleteCommand_NothingToChange(t *testing.T) var result struct { Status string `json:"status"` } - err := json.Unmarshal(buf.Bytes(), &result) + err := unmarshalWithProtos(buf.Bytes(), &result) require.NoError(t, err) expected := struct { Status string `json:"status"` diff --git a/temporalcloudcli/commands.namespace.codec_test.go b/temporalcloudcli/commands.namespace.codec_test.go index bd67f07..c69c8c4 100644 --- a/temporalcloudcli/commands.namespace.codec_test.go +++ b/temporalcloudcli/commands.namespace.codec_test.go @@ -7,11 +7,13 @@ import ( "errors" "testing" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/testing/protocmp" "github.com/temporalio/cloud-cli/internal/namespace" "github.com/temporalio/cloud-cli/temporalcloudcli" @@ -47,7 +49,7 @@ func TestCloudNamespaceCodecGetCommand_NoCodecServer(t *testing.T) { Namespace string `json:"Namespace"` Spec json.RawMessage `json:"Spec"` } - require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &result)) assert.Equal(t, "test-namespace.test-account", result.Namespace) assert.Equal(t, "null", string(result.Spec)) } @@ -135,11 +137,11 @@ func TestCloudNamespaceCodecSetCommand_Success(t *testing.T) { }, assertResult: func(t *testing.T, buf bytes.Buffer) { var result temporalcloudcli.MutationResult - require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, temporalcloudcli.MutationResult{ + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &result)) + assert.Empty(t, cmp.Diff(temporalcloudcli.MutationResult{ AsyncOp: &operation.AsyncOperation{Id: "test-operation-id"}, ID: "test-namespace.test-account", - }, result) + }, result, protocmp.Transform())) }, }, { @@ -211,11 +213,11 @@ func TestCloudNamespaceCodecSetCommand_WithCustomErrorMessage(t *testing.T) { require.NoError(t, capturedErr) var result temporalcloudcli.MutationResult - require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, temporalcloudcli.MutationResult{ + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &result)) + assert.Empty(t, cmp.Diff(temporalcloudcli.MutationResult{ AsyncOp: &operation.AsyncOperation{Id: "test-operation-id"}, ID: "test-namespace.test-account", - }, result) + }, result, protocmp.Transform())) } func TestCloudNamespaceCodecSetCommand_Error(t *testing.T) { @@ -260,7 +262,7 @@ func TestCloudNamespaceCodecSetCommand_NothingToChange(t *testing.T) { assertResult: func(t *testing.T, capturedErr error, buf bytes.Buffer) { require.NoError(t, capturedErr) var result temporalcloudcli.Result - require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &result)) assert.Equal(t, temporalcloudcli.Result{Status: "unchanged"}, result) }, }, @@ -364,11 +366,11 @@ func TestCloudNamespaceCodecDeleteCommand_Success(t *testing.T) { }, assertResult: func(t *testing.T, buf bytes.Buffer) { var result temporalcloudcli.MutationResult - require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, temporalcloudcli.MutationResult{ + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &result)) + assert.Empty(t, cmp.Diff(temporalcloudcli.MutationResult{ AsyncOp: &operation.AsyncOperation{Id: "test-operation-id"}, ID: "test-namespace.test-account", - }, result) + }, result, protocmp.Transform())) }, }, { @@ -499,7 +501,7 @@ func TestCloudNamespaceCodecDeleteCommand_NothingToChange(t *testing.T) { assertResult: func(t *testing.T, capturedErr error, buf bytes.Buffer) { require.NoError(t, capturedErr) var result temporalcloudcli.Result - require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &result)) assert.Equal(t, temporalcloudcli.Result{Status: "unchanged"}, result) }, }, diff --git a/temporalcloudcli/commands.namespace.retention_test.go b/temporalcloudcli/commands.namespace.retention_test.go index 9453939..108676c 100644 --- a/temporalcloudcli/commands.namespace.retention_test.go +++ b/temporalcloudcli/commands.namespace.retention_test.go @@ -3,7 +3,6 @@ package temporalcloudcli_test import ( "bytes" "context" - "encoding/json" "errors" "testing" @@ -51,7 +50,7 @@ func TestGetRetention_Success(t *testing.T) { RetentionDays int32 `json:"retentionDays"` } var out retentionOutput - require.NoError(t, json.Unmarshal(buf.Bytes(), &out)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &out)) assert.Equal(t, retentionOutput{Namespace: "my-namespace", RetentionDays: 14}, out) } diff --git a/temporalcloudcli/commands.namespace.search_attribute_test.go b/temporalcloudcli/commands.namespace.search_attribute_test.go index 72ca6ef..92426d8 100644 --- a/temporalcloudcli/commands.namespace.search_attribute_test.go +++ b/temporalcloudcli/commands.namespace.search_attribute_test.go @@ -3,10 +3,10 @@ package temporalcloudcli_test import ( "bytes" "context" - "encoding/json" "errors" "testing" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -14,6 +14,7 @@ import ( "go.temporal.io/cloud-sdk/api/operation/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/testing/protocmp" "github.com/temporalio/cloud-cli/internal/namespace" "github.com/temporalio/cloud-cli/temporalcloudcli" @@ -46,7 +47,7 @@ func TestCloudNamespaceSearchAttributeListCommand_Success(t *testing.T) { var result struct { SearchAttributes []temporalcloudcli.SearchAttributeOutput `json:"SearchAttributes"` } - require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &result)) assert.Equal(t, []temporalcloudcli.SearchAttributeOutput{{Name: "MyField", Type: "Keyword"}}, result.SearchAttributes) } @@ -124,11 +125,11 @@ func TestCloudNamespaceSearchAttributeCreateCommand_Success(t *testing.T) { }, assertResult: func(t *testing.T, buf bytes.Buffer) { var result temporalcloudcli.MutationResult - require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, temporalcloudcli.MutationResult{ + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &result)) + assert.Empty(t, cmp.Diff(temporalcloudcli.MutationResult{ AsyncOp: &operation.AsyncOperation{Id: "test-operation-id"}, ID: "test-namespace.test-account", - }, result) + }, result, protocmp.Transform())) }, }, { @@ -243,7 +244,7 @@ func TestCloudNamespaceSearchAttributeCreateCommand_NothingToChange(t *testing.T assertResult: func(t *testing.T, capturedErr error, buf bytes.Buffer) { require.NoError(t, capturedErr) var result temporalcloudcli.Result - require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &result)) assert.Equal(t, temporalcloudcli.Result{Status: "unchanged"}, result) }, }, @@ -390,11 +391,11 @@ func TestCloudNamespaceSearchAttributeRenameCommand_Success(t *testing.T) { }, assertResult: func(t *testing.T, buf bytes.Buffer) { var result temporalcloudcli.MutationResult - require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, temporalcloudcli.MutationResult{ + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &result)) + assert.Empty(t, cmp.Diff(temporalcloudcli.MutationResult{ AsyncOp: &operation.AsyncOperation{Id: "test-operation-id"}, ID: "test-namespace.test-account", - }, result) + }, result, protocmp.Transform())) }, }, { diff --git a/temporalcloudcli/commands.namespace_test.go b/temporalcloudcli/commands.namespace_test.go index d9bb6a1..ba9a55a 100644 --- a/temporalcloudcli/commands.namespace_test.go +++ b/temporalcloudcli/commands.namespace_test.go @@ -4,7 +4,6 @@ package temporalcloudcli_test import ( - "encoding/json" "fmt" "io" "strings" @@ -14,7 +13,6 @@ import ( "go.temporal.io/cloud-sdk/api/cloudservice/v1" namespace "go.temporal.io/cloud-sdk/api/namespace/v1" resource "go.temporal.io/cloud-sdk/api/resource/v1" - "google.golang.org/protobuf/encoding/protojson" ) const ( @@ -58,7 +56,7 @@ func (s *SharedServerSuite) TestNamespaceCreate() { s.Suite.Require().NoError(err) result := &temporalcloudcli.MutationResult{} - err = json.Unmarshal(buf, result) + err = unmarshalWithProtos(buf, result) s.Suite.Require().NoError(err) s.Suite.Require().NotEmpty(result.ID) s.Suite.Require().NotNil(result.AsyncOp) @@ -125,7 +123,7 @@ func (s *SharedServerSuite) testnamespaceCRUD() { buf, err = io.ReadAll(&res.Stdout) s.Suite.Require().NoError(err) result := &temporalcloudcli.MutationResult{} - err = json.Unmarshal(buf, result) + err = unmarshalWithProtos(buf, result) s.Suite.Require().NoError(err) namespaceID := result.ID @@ -149,7 +147,7 @@ func (s *SharedServerSuite) testnamespaceCRUD() { s.Suite.Require().NoError(err) readNamespace := &namespace.Namespace{} - err = protojson.Unmarshal(buf, readNamespace) + err = unmarshalWithProtos(buf, readNamespace) s.Suite.Require().NoError(err) // compare it to the inputted spec @@ -194,7 +192,7 @@ func (s *SharedServerSuite) testnamespaceCRUD() { buf, err = io.ReadAll(&res.Stdout) s.Suite.Require().NoError(err) result = &temporalcloudcli.MutationResult{} - err = json.Unmarshal(buf, result) + err = unmarshalWithProtos(buf, result) s.Suite.Require().NoError(err) // make sure ns apply is completed @@ -216,7 +214,7 @@ func (s *SharedServerSuite) testnamespaceCRUD() { s.Suite.Require().NoError(err) readNamespace = &namespace.Namespace{} - err = protojson.Unmarshal(buf, readNamespace) + err = unmarshalWithProtos(buf, readNamespace) s.Suite.Require().NoError(err) // compare it to the inputted spec diff --git a/temporalcloudcli/commands.testing.go b/temporalcloudcli/commands.testing.go index 4bca38f..412f57b 100644 --- a/temporalcloudcli/commands.testing.go +++ b/temporalcloudcli/commands.testing.go @@ -3,14 +3,14 @@ package temporalcloudcli import ( "bytes" "context" - "encoding/json" + jsonv2 "encoding/json/v2" "testing" "time" + "go.temporal.io/api/temporalproto" cloudservice "go.temporal.io/cloud-sdk/api/cloudservice/v1" "go.temporal.io/cloud-sdk/api/operation/v1" "google.golang.org/grpc" - "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "github.com/stretchr/testify/assert" @@ -199,13 +199,12 @@ func TestCommand(t *testing.T, command CommandIfc, opts TestCommandOptions) { if opts.ExpectedOutputJson != nil { var js []byte var err error - if protoMessage, ok := opts.ExpectedOutputJson.(proto.Message); ok { - js, err = protojson.MarshalOptions{ - EmitDefaultValues: opts.ExpectedOutputJsonEmitDefaults, - }.Marshal(protoMessage) - } else { - js, err = json.Marshal(opts.ExpectedOutputJson) - } + customMarshalers := jsonv2.WithMarshalers(jsonv2.JoinMarshalers(jsonv2.MarshalFunc(func(m proto.Message) ([]byte, error) { + protoOpts := temporalproto.CustomJSONMarshalOptions{EmitDefaultValues: opts.ExpectedOutputJsonEmitDefaults} + return protoOpts.Marshal(m) + }))) + + js, err = jsonv2.Marshal(opts.ExpectedOutputJson, customMarshalers) assert.NoError(t, err) assert.JSONEq(t, string(js), printerBuf.String()) } diff --git a/temporalcloudcli/commands.whoami_test.go b/temporalcloudcli/commands.whoami_test.go index 3519c4b..2daa2e3 100644 --- a/temporalcloudcli/commands.whoami_test.go +++ b/temporalcloudcli/commands.whoami_test.go @@ -8,7 +8,6 @@ import ( "io" "go.temporal.io/cloud-sdk/api/cloudservice/v1" - "google.golang.org/protobuf/encoding/protojson" ) func (s *SharedServerSuite) TestWhoami() { @@ -23,7 +22,7 @@ func (s *SharedServerSuite) TestWhoami() { s.Suite.Require().NoError(err) identity := &cloudservice.GetCurrentIdentityResponse{} - err = protojson.Unmarshal(buf, identity) + err = unmarshalWithProtos(buf, identity) s.Suite.Require().NoError(err) // The response must identify the caller as either a user or a service account. diff --git a/temporalcloudcli/common_test.go b/temporalcloudcli/common_test.go index fe39da4..c6e70f7 100644 --- a/temporalcloudcli/common_test.go +++ b/temporalcloudcli/common_test.go @@ -3,7 +3,7 @@ package temporalcloudcli_test import ( "bytes" "context" - "encoding/json" + jsonv2 "encoding/json/v2" "errors" "fmt" "regexp" @@ -11,14 +11,18 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/temporalio/cloud-cli/temporalcloudcli" "github.com/temporalio/cloud-cli/temporalcloudcli/internal/printer" cmdmock "github.com/temporalio/cloud-cli/temporalcloudcli/mock" + "go.temporal.io/api/temporalproto" operation "go.temporal.io/cloud-sdk/api/operation/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" ) type CommandHarness struct { @@ -52,6 +56,18 @@ func (h *CommandHarness) ContainsOnSameLine(text string, pieces ...string) { h.NoError(AssertContainsOnSameLine(text, pieces...)) } +func unmarshalWithProtos(b []byte, out any) error { + opts := jsonv2.WithUnmarshalers( + jsonv2.JoinUnmarshalers( + jsonv2.UnmarshalFunc(func(b []byte, m proto.Message) error { + protoOpts := temporalproto.CustomJSONUnmarshalOptions{} + return protoOpts.Unmarshal(b, m) + }), + ), + ) + return jsonv2.Unmarshal(b, out, opts, jsonv2.RejectUnknownMembers(true)) +} + func AssertContainsOnSameLine(text string, pieces ...string) error { // Build regex pattern based on pieces pattern := "" @@ -168,8 +184,8 @@ func TestAsyncOperationHandler_Async(t *testing.T) { require.NoError(t, err) var out temporalcloudcli.MutationResult - require.NoError(t, json.Unmarshal(buf.Bytes(), &out)) - assert.Equal(t, temporalcloudcli.MutationResult{AsyncOp: op, ID: "my-namespace"}, out) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &out)) + assert.Empty(t, cmp.Diff(temporalcloudcli.MutationResult{AsyncOp: op, ID: "my-namespace"}, out, protocmp.Transform())) } func TestAsyncOperationHandler_Sync(t *testing.T) { @@ -214,7 +230,7 @@ func TestAsyncOperationHandler_HandleUpdateErr_NothingToChange_Idempotent(t *tes require.NoError(t, err) var out temporalcloudcli.Result - require.NoError(t, json.Unmarshal(buf.Bytes(), &out)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &out)) assert.Equal(t, temporalcloudcli.Result{Status: "unchanged"}, out) } @@ -253,7 +269,7 @@ func TestAsyncOperationHandler_HandleCreateErr_AlreadyExists_Idempotent(t *testi require.NoError(t, err) var out temporalcloudcli.Result - require.NoError(t, json.Unmarshal(buf.Bytes(), &out)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &out)) assert.Equal(t, temporalcloudcli.Result{Status: "unchanged"}, out) } @@ -280,7 +296,7 @@ func TestAsyncOperationHandler_HandleDeleteErr_NotFound_Idempotent(t *testing.T) require.NoError(t, err) var out temporalcloudcli.Result - require.NoError(t, json.Unmarshal(buf.Bytes(), &out)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &out)) assert.Equal(t, temporalcloudcli.Result{Status: "unchanged"}, out) } diff --git a/temporalcloudcli/internal/printer/printer.go b/temporalcloudcli/internal/printer/printer.go index 54e00ca..7d338c3 100644 --- a/temporalcloudcli/internal/printer/printer.go +++ b/temporalcloudcli/internal/printer/printer.go @@ -3,6 +3,8 @@ package printer import ( "encoding/base64" "encoding/json" + jsontext "encoding/json/jsontext" + jsonv2 "encoding/json/v2" "fmt" "io" "maps" @@ -267,20 +269,29 @@ func (p *Printer) printJSON(v any, options StructuredOptions) error { } func (p *Printer) jsonVal(v any, indent string, shorthandPayloads, emitDefaultValues bool) ([]byte, error) { - // Use proto JSON if a proto message - if protoMessage, ok := v.(proto.Message); ok { - opts := temporalproto.CustomJSONMarshalOptions{Indent: indent, EmitDefaultValues: emitDefaultValues} + opts := make([]jsonv2.Options, 0, 4) + // Preserve parts of jsonv1 behavior to make the CLI more resilient + opts = append( + opts, + // Emit a placeholder rather than hard fail + jsontext.AllowInvalidUTF8(true), + // Emit durations as number of nanoseconds, rather than hard fail + json.FormatDurationAsNano(true), + ) + if indent != "" { + // This will also re-indent any json created by custom marshalers, so we don't need to pass indentation rules through to them + opts = append(opts, jsontext.WithIndent(indent)) + } + + opts = append(opts, jsonv2.WithMarshalers(jsonv2.JoinMarshalers(jsonv2.MarshalFunc(func(m proto.Message) ([]byte, error) { + protoOpts := temporalproto.CustomJSONMarshalOptions{EmitDefaultValues: emitDefaultValues} if shorthandPayloads { - opts.Metadata = map[string]any{common.EnablePayloadShorthandMetadataKey: true} + protoOpts.Metadata = map[string]any{common.EnablePayloadShorthandMetadataKey: true} } - return opts.Marshal(protoMessage) - } + return protoOpts.Marshal(m) + })))) - // Normal JSON encoding - if indent != "" { - return json.MarshalIndent(v, "", indent) - } - return json.Marshal(v) + return jsonv2.Marshal(v, opts...) } type col struct { diff --git a/temporalcloudcli/internal/printer/printer_test.go b/temporalcloudcli/internal/printer/printer_test.go index f7b898e..435fd83 100644 --- a/temporalcloudcli/internal/printer/printer_test.go +++ b/temporalcloudcli/internal/printer/printer_test.go @@ -3,6 +3,7 @@ package printer import ( "bytes" "encoding/json" + jsonv2 "encoding/json/v2" "strings" "testing" "time" @@ -10,9 +11,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.temporal.io/api/temporalproto" namespacev1 "go.temporal.io/cloud-sdk/api/namespace/v1" "go.temporal.io/cloud-sdk/api/operation/v1" "go.temporal.io/cloud-sdk/api/resource/v1" + "google.golang.org/protobuf/proto" ) func TestPrinter_Text(t *testing.T) { @@ -72,6 +75,18 @@ func normalizeMultiline(s string) string { return ret } +func unmarshalWithProtos(b []byte, out any) error { + opts := jsonv2.WithUnmarshalers( + jsonv2.JoinUnmarshalers( + jsonv2.UnmarshalFunc(func(b []byte, m proto.Message) error { + protoOpts := temporalproto.CustomJSONUnmarshalOptions{} + return protoOpts.Unmarshal(b, m) + }), + ), + ) + return jsonv2.Unmarshal(b, out, opts, jsonv2.RejectUnknownMembers(true)) +} + func TestPrinter_JSON(t *testing.T) { var buf bytes.Buffer @@ -353,7 +368,7 @@ func TestPrinter_PrintDiff_JSON(t *testing.T) { p := Printer{Output: &buf, JSON: true} require.NoError(t, p.PrintDiff(a, b, DiffOptions{})) var result map[string]json.RawMessage - require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &result)) require.Contains(t, result, "before") require.Contains(t, result, "after") require.Contains(t, string(result["before"]), "old") @@ -364,7 +379,7 @@ func TestPrinter_PrintDiff_JSON(t *testing.T) { p.JSONIndent = " " require.NoError(t, p.PrintDiff(a, b, DiffOptions{})) var prettyResult map[string]json.RawMessage - require.NoError(t, json.Unmarshal(buf.Bytes(), &prettyResult)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &prettyResult)) require.Contains(t, prettyResult, "before") require.Contains(t, prettyResult, "after") } @@ -434,7 +449,7 @@ func TestPrinter_PrintResource(t *testing.T) { p.JSONIndent = " " require.NoError(t, p.PrintResource(resource, PrintResourceOptions{})) var jsonResult map[string]any - require.NoError(t, json.Unmarshal(buf.Bytes(), &jsonResult)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &jsonResult)) require.Equal(t, "my-resource", jsonResult["Name"]) } @@ -521,7 +536,7 @@ func TestPrinter_PrintResourceList(t *testing.T) { p.JSON = true require.NoError(t, p.PrintResourceList(response, PrintResourceOptions{}, TableOptions{})) var jsonResult map[string]any - require.NoError(t, json.Unmarshal(buf.Bytes(), &jsonResult)) + require.NoError(t, unmarshalWithProtos(buf.Bytes(), &jsonResult)) require.NotNil(t, jsonResult["Resources"]) }