diff --git a/cmd/invoke.go b/cmd/invoke.go index 7dabd964..6558f23d 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -6,10 +6,8 @@ package cmd import ( - "fmt" "os" - "github.com/dapr/cli/pkg/invoke" "github.com/dapr/cli/pkg/print" "github.com/spf13/cobra" ) @@ -24,16 +22,11 @@ var InvokeCmd = &cobra.Command{ Use: "invoke", Short: "Invokes a Dapr app with an optional payload (deprecated, use invokePost)", Run: func(cmd *cobra.Command, args []string) { - response, err := invoke.Post(invokeAppID, invokeAppMethod, invokePayload) + err := invokePost(invokeAppID, invokeAppMethod, invokePayload) if err != nil { - print.FailureStatusEvent(os.Stdout, fmt.Sprintf("Error invoking app %s: %s", invokeAppID, err)) - return + // exit with error + os.Exit(1) } - - if response != "" { - fmt.Println(response) - } - print.SuccessStatusEvent(os.Stdout, "App invoked successfully") }, } diff --git a/cmd/invokeGet.go b/cmd/invokeGet.go index 5029a990..29557467 100644 --- a/cmd/invokeGet.go +++ b/cmd/invokeGet.go @@ -4,8 +4,8 @@ import ( "fmt" "os" - "github.com/dapr/cli/pkg/invoke" "github.com/dapr/cli/pkg/print" + "github.com/dapr/cli/pkg/standalone" "github.com/spf13/cobra" ) @@ -14,11 +14,12 @@ var invokeGetCmd = &cobra.Command{ Use: "invokeGet", Short: "Issue HTTP GET to Dapr app", Run: func(cmd *cobra.Command, args []string) { - response, err := invoke.Get(invokeAppID, invokeAppMethod) + client := standalone.NewClient() + response, err := client.InvokeGet(invokeAppID, invokeAppMethod) if err != nil { - print.FailureStatusEvent(os.Stdout, fmt.Sprintf("Error invoking app %s: %s", invokeAppID, err)) - - return + print.FailureStatusEvent(os.Stdout, fmt.Sprintf("error invoking app %s: %s", invokeAppID, err)) + // exit with error + os.Exit(1) } if response != "" { diff --git a/cmd/invokePost.go b/cmd/invokePost.go index a2e06cf3..618cc1ca 100644 --- a/cmd/invokePost.go +++ b/cmd/invokePost.go @@ -9,8 +9,8 @@ import ( "fmt" "os" - "github.com/dapr/cli/pkg/invoke" "github.com/dapr/cli/pkg/print" + "github.com/dapr/cli/pkg/standalone" "github.com/spf13/cobra" ) @@ -18,21 +18,30 @@ var invokePostCmd = &cobra.Command{ Use: "invokePost", Short: "Issue HTTP POST to Dapr app with an optional payload", Run: func(cmd *cobra.Command, args []string) { - response, err := invoke.Post(invokeAppID, invokeAppMethod, invokePayload) + err := invokePost(invokeAppID, invokeAppMethod, invokePayload) if err != nil { - print.FailureStatusEvent(os.Stdout, fmt.Sprintf("Error invoking app %s: %s", invokeAppID, err)) - - return + // exit with error + os.Exit(1) } - - if response != "" { - fmt.Println(response) - } - print.SuccessStatusEvent(os.Stdout, fmt.Sprintf("HTTP Post to method %s invoked successfully", invokeAppMethod)) }, } +func invokePost(invokeAppID, invokeAppMethod, invokePayload string) error { + client := standalone.NewClient() + response, err := client.InvokePost(invokeAppID, invokeAppMethod, invokePayload) + if err != nil { + er := fmt.Errorf("error invoking app %s: %s", invokeAppID, err) + print.FailureStatusEvent(os.Stdout, er.Error()) + return er + } + + if response != "" { + fmt.Println(response) + } + return nil +} + func init() { invokePostCmd.Flags().StringVarP(&invokeAppID, "app-id", "a", "", "the app id to invoke") invokePostCmd.Flags().StringVarP(&invokeAppMethod, "method", "m", "", "the method to invoke") diff --git a/cmd/publish.go b/cmd/publish.go index 745a63e5..5aeb180c 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -10,7 +10,7 @@ import ( "os" "github.com/dapr/cli/pkg/print" - "github.com/dapr/cli/pkg/publish" + "github.com/dapr/cli/pkg/standalone" "github.com/spf13/cobra" ) @@ -24,10 +24,11 @@ var PublishCmd = &cobra.Command{ Use: "publish", Short: "Publish an event to multiple consumers", Run: func(cmd *cobra.Command, args []string) { - err := publish.SendPayloadToTopic(publishTopic, publishPayload, pubsubName) + client := standalone.NewClient() + err := client.Publish(publishTopic, publishPayload, pubsubName) if err != nil { print.FailureStatusEvent(os.Stdout, fmt.Sprintf("Error publishing topic %s: %s", publishTopic, err)) - return + os.Exit(1) } print.SuccessStatusEvent(os.Stdout, "Event published successfully") diff --git a/pkg/metadata/metadata_test.go b/pkg/metadata/metadata_test.go new file mode 100644 index 00000000..7f0c75ea --- /dev/null +++ b/pkg/metadata/metadata_test.go @@ -0,0 +1,19 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package metadata + +import ( + "fmt" + "testing" + + "github.com/dapr/cli/pkg/api" + "github.com/stretchr/testify/assert" +) + +func TestMakeMetadataGetEndpoint(t *testing.T) { + actual := makeMetadataGetEndpoint(9999) + assert.Equal(t, fmt.Sprintf("http://127.0.0.1:9999/v%s/metadata", api.RuntimeAPIVersion), actual, "expected strings to match") +} diff --git a/pkg/standalone/client.go b/pkg/standalone/client.go new file mode 100644 index 00000000..1e7baac9 --- /dev/null +++ b/pkg/standalone/client.go @@ -0,0 +1,31 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package standalone + +type DaprProcess interface { + List() ([]ListOutput, error) +} + +type daprProcess struct { +} + +// Client is the interface the wraps all the methods exposed by the Dapr CLI. +type Client interface { + // InvokeGet is used to invoke a method on a Dapr application with GET verb. + InvokeGet(appID, method string) (string, error) + // InvokePost is used to invoke a method on a Dapr application with POST verb. + InvokePost(appID, method, payload string) (string, error) + // Publish is used to publish event to a topic in a pubsub. + Publish(topic, payload, pubsubName string) error +} + +type Standalone struct { + process DaprProcess +} + +func NewClient() Client { + return &Standalone{process: &daprProcess{}} +} diff --git a/pkg/invoke/invoke.go b/pkg/standalone/invoke.go similarity index 77% rename from pkg/invoke/invoke.go rename to pkg/standalone/invoke.go index ac5f26c1..32f85e0e 100644 --- a/pkg/invoke/invoke.go +++ b/pkg/standalone/invoke.go @@ -3,7 +3,7 @@ // Licensed under the MIT License. // ------------------------------------------------------------ -package invoke +package standalone import ( "bytes" @@ -12,12 +12,11 @@ import ( "net/http" "github.com/dapr/cli/pkg/api" - "github.com/dapr/cli/pkg/standalone" ) -// Get invokes the application via HTTP GET. -func Get(appID, method string) (string, error) { - list, err := standalone.List() +// InvokeGet invokes the application via HTTP GET. +func (s *Standalone) InvokeGet(appID, method string) (string, error) { + list, err := s.process.List() if err != nil { return "", err } @@ -38,9 +37,9 @@ func Get(appID, method string) (string, error) { return "", fmt.Errorf("app ID %s not found", appID) } -// Post invokes the application via HTTP POST. -func Post(appID, method, payload string) (string, error) { - list, err := standalone.List() +// InvokePost invokes the application via HTTP POST. +func (s *Standalone) InvokePost(appID, method, payload string) (string, error) { + list, err := s.process.List() if err != nil { return "", err } @@ -62,7 +61,7 @@ func Post(appID, method, payload string) (string, error) { return "", fmt.Errorf("app ID %s not found", appID) } -func makeEndpoint(lo standalone.ListOutput, method string) string { +func makeEndpoint(lo ListOutput, method string) string { return fmt.Sprintf("http://127.0.0.1:%s/v%s/invoke/%s/method/%s", fmt.Sprintf("%v", lo.HTTPPort), api.RuntimeAPIVersion, lo.AppID, method) } diff --git a/pkg/standalone/invoke_test.go b/pkg/standalone/invoke_test.go new file mode 100644 index 00000000..567e3c95 --- /dev/null +++ b/pkg/standalone/invoke_test.go @@ -0,0 +1,107 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package standalone + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestInvoke(t *testing.T) { + testCases := []struct { + name string + errorExpected bool + errString string + appID string + method string + lo ListOutput + listErr error + expectedPath string + postResponse string + resp string + }{ + { + name: "list apps error", + errorExpected: true, + errString: assert.AnError.Error(), + listErr: assert.AnError, + }, + { + name: "appID not found", + errorExpected: true, + appID: "invalid", + errString: "app ID invalid not found", + lo: ListOutput{ + AppID: "testapp", + }, + }, + { + name: "appID found successful invoke empty response", + appID: "testapp", + method: "test", + lo: ListOutput{ + AppID: "testapp", + }, + }, + { + name: "appID found successful invoke", + appID: "testapp", + method: "test", + lo: ListOutput{ + AppID: "testapp", + }, + expectedPath: "/v1.0/invoke/testapp/method/test", + postResponse: "test payload", + resp: "successful invoke", + }, + } + + for _, tc := range testCases { + t.Run(tc.name+" get", func(t *testing.T) { + ts, port := getTestServer(tc.expectedPath, tc.resp) + ts.Start() + defer ts.Close() + tc.lo.HTTPPort = port + client := &Standalone{ + process: &mockDaprProcess{ + Lo: []ListOutput{ + tc.lo, + }, + Err: tc.listErr, + }, + } + res, err := client.InvokeGet(tc.appID, tc.method) + if tc.errorExpected { + assert.Error(t, err, "expected an error") + assert.Equal(t, tc.errString, err.Error(), "expected error strings to match") + } else { + assert.NoError(t, err, "expected no error") + assert.Equal(t, tc.resp, res, "expected response to match") + } + }) + t.Run(tc.name+" post", func(t *testing.T) { + ts, port := getTestServer(tc.expectedPath, tc.resp) + ts.Start() + defer ts.Close() + tc.lo.HTTPPort = port + client := &Standalone{ + process: &mockDaprProcess{ + Lo: []ListOutput{tc.lo}, + Err: tc.listErr, + }, + } + res, err := client.InvokePost(tc.appID, tc.method, "test payload") + if tc.errorExpected { + assert.Error(t, err, "expected an error") + assert.Equal(t, tc.errString, err.Error(), "expected error strings to match") + } else { + assert.NoError(t, err, "expected no error") + assert.Equal(t, tc.postResponse, res, "expected response to match") + } + }) + } +} diff --git a/pkg/standalone/list.go b/pkg/standalone/list.go index 7f990d0a..43441c69 100644 --- a/pkg/standalone/list.go +++ b/pkg/standalone/list.go @@ -41,6 +41,10 @@ type runData struct { appCmd string } +func (d *daprProcess) List() ([]ListOutput, error) { + return List() +} + // List outputs all the applications. func List() ([]ListOutput, error) { list := []ListOutput{} diff --git a/pkg/publish/publish.go b/pkg/standalone/publish.go similarity index 71% rename from pkg/publish/publish.go rename to pkg/standalone/publish.go index be2a8960..7506c858 100644 --- a/pkg/publish/publish.go +++ b/pkg/standalone/publish.go @@ -3,7 +3,7 @@ // Licensed under the MIT License. // ------------------------------------------------------------ -package publish +package standalone import ( "bytes" @@ -12,11 +12,10 @@ import ( "net/http" "github.com/dapr/cli/pkg/api" - "github.com/dapr/cli/pkg/standalone" ) -// SendPayloadToTopic publishes the topic. -func SendPayloadToTopic(topic, payload, pubsubName string) error { +// Publish publishes payload to topic in pubsub referenced by pubsubName. +func (s *Standalone) Publish(topic, payload, pubsubName string) error { if topic == "" { return errors.New("topic is missing") } @@ -24,7 +23,7 @@ func SendPayloadToTopic(topic, payload, pubsubName string) error { return errors.New("pubsubName is missing") } - l, err := standalone.List() + l, err := s.process.List() if err != nil { return err } @@ -43,19 +42,18 @@ func SendPayloadToTopic(topic, payload, pubsubName string) error { url := fmt.Sprintf("http://localhost:%s/v%s/publish/%s/%s", fmt.Sprintf("%v", daprHTTPPort), api.RuntimeAPIVersion, pubsubName, topic) // nolint: gosec r, err := http.Post(url, "application/json", bytes.NewBuffer(b)) - - if r != nil { - defer r.Body.Close() - } - if err != nil { return err } + defer r.Body.Close() + if r.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code %d on publishing to %s in %s", r.StatusCode, topic, pubsubName) + } return nil } -func getDaprHTTPPort(list []standalone.ListOutput) (int, error) { +func getDaprHTTPPort(list []ListOutput) (int, error) { for i := 0; i < len(list); i++ { if list[i].AppID != "" { return list[i].HTTPPort, nil diff --git a/pkg/standalone/publish_test.go b/pkg/standalone/publish_test.go new file mode 100644 index 00000000..60132fd5 --- /dev/null +++ b/pkg/standalone/publish_test.go @@ -0,0 +1,96 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package standalone + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPublish(t *testing.T) { + testCases := []struct { + name string + pubsubName string + payload string + topic string + lo ListOutput + listErr error + expectedPath string + postResponse string + resp string + errorExpected bool + errString string + }{ + { + name: "test empty topic", + payload: "test", + pubsubName: "test", + errString: "topic is missing", + errorExpected: true, + }, + { + name: "test empty pubsubName", + payload: "test", + topic: "test", + errString: "pubsubName is missing", + errorExpected: true, + }, + { + name: "test list error", + payload: "test", + topic: "test", + pubsubName: "test", + listErr: assert.AnError, + errString: assert.AnError.Error(), + errorExpected: true, + }, + { + name: "test empty appID in list output", + payload: "test", + topic: "test", + pubsubName: "test", + lo: ListOutput{ + // empty appID + Command: "test", + }, + errString: "couldn't find a running Dapr instance", + errorExpected: true, + }, + { + name: "successful call", + pubsubName: "testPubsubName", + topic: "testTopic", + payload: "test payload", + expectedPath: "/v1.0/publish/testPubsubName/testTopic", + postResponse: "test payload", + lo: ListOutput{ + AppID: "notempty", + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ts, port := getTestServer(tc.expectedPath, tc.resp) + ts.Start() + defer ts.Close() + tc.lo.HTTPPort = port + client := &Standalone{ + process: &mockDaprProcess{ + Lo: []ListOutput{tc.lo}, + Err: tc.listErr, + }, + } + err := client.Publish(tc.topic, tc.payload, tc.pubsubName) + if tc.errorExpected { + assert.Error(t, err, "expected an error") + assert.Equal(t, tc.errString, err.Error(), "expected error strings to match") + } else { + assert.NoError(t, err, "expected no error") + } + }) + } +} diff --git a/pkg/standalone/testutils.go b/pkg/standalone/testutils.go new file mode 100644 index 00000000..a02777c6 --- /dev/null +++ b/pkg/standalone/testutils.go @@ -0,0 +1,42 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package standalone + +import ( + "bytes" + "net" + "net/http" + "net/http/httptest" +) + +type mockDaprProcess struct { + Lo []ListOutput + Err error +} + +func (m *mockDaprProcess) List() ([]ListOutput, error) { + return m.Lo, m.Err +} + +func getTestServer(expectedPath, resp string) (*httptest.Server, int) { + ts := httptest.NewUnstartedServer(http.HandlerFunc(func( + w http.ResponseWriter, r *http.Request) { + if r.RequestURI != expectedPath { + w.WriteHeader(http.StatusInternalServerError) + + return + } + if r.Method == http.MethodPost { + buf := new(bytes.Buffer) + buf.ReadFrom(r.Body) + w.Write(buf.Bytes()) + } else if r.Method == http.MethodGet { + w.Write([]byte(resp)) + } + })) + + return ts, ts.Listener.Addr().(*net.TCPAddr).Port +}