diff --git a/api/account.go b/api/account.go index 264df67..18d3d39 100644 --- a/api/account.go +++ b/api/account.go @@ -8,9 +8,9 @@ import ( ) //isAPIKeyValid returns true if the stored API key is valid. -func isAPIKeyValid() bool { +func (p *PusherApi) isAPIKeyValid() bool { if viper.GetString("token") != "" { - _, err := GetAllApps() + _, err := p.GetAllApps() if err == nil { return true } @@ -18,8 +18,8 @@ func isAPIKeyValid() bool { return false } -func validateKeyOrDie() { - if !isAPIKeyValid() { +func (p *PusherApi) validateKeyOrDie() { + if !p.isAPIKeyValid() { fmt.Println("Your API key isn't valid. Add one with the `login` command.") os.Exit(1) } diff --git a/api/app.go b/api/app.go index 5d3f4cb..3c421b7 100644 --- a/api/app.go +++ b/api/app.go @@ -12,10 +12,10 @@ type App struct { Cluster string `json:"cluster"` } -const getAppsAPIEndpoint = "/apps.json" +const GetAppsAPIEndpoint = "/apps.json" -func GetAllApps() ([]App, error) { - response, err := makeRequest("GET", getAppsAPIEndpoint, nil) +func (p *PusherApi) GetAllApps() ([]App, error) { + response, err := p.makeRequest("GET", GetAppsAPIEndpoint, nil) if err != nil { return nil, err } @@ -29,8 +29,8 @@ func GetAllApps() ([]App, error) { return apps, nil } -func GetApp(appID string) (*App, error) { - apps, err := GetAllApps() +func (p *PusherApi) GetApp(appID string) (*App, error) { + apps, err := p.GetAllApps() if err != nil { return nil, err } diff --git a/api/function.go b/api/function.go new file mode 100644 index 0000000..ad7d44d --- /dev/null +++ b/api/function.go @@ -0,0 +1,173 @@ +package api + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" +) + +type FunctionService interface { + GetAllFunctionsForApp(name string) ([]Function, error) + CreateFunction(appID string, name string, events []string, body string) (Function, error) + DeleteFunction(appID string, functionID string) error + GetFunction(appID string, functionID string) (Function, error) + UpdateFunction(appID string, functionID string, name string, events []string, body string) (Function, error) +} + +type Function struct { + ID int `json:"id"` + Name string `json:"name"` + Events []string `json:"events"` + Body string `json:"body"` +} + +type FunctionRequestBody struct { + Function FunctionRequestBodyFunction `json:"function"` +} + +type FunctionRequestBodyFunction struct { + Name string `json:"name"` + Events []string `json:"events"` + Body string `json:"body"` +} + +const FunctionsApiEndpoint = "/apps/%s/functions.json" +const FunctionApiEndpoint = "/apps/%s/functions/%s.json" + +func NewFunctionRequestBody(name string, events []string, body string) FunctionRequestBody { + return FunctionRequestBody{ + Function: FunctionRequestBodyFunction{ + Name: name, + Events: events, + Body: body, + }, + } +} + +func (p *PusherApi) GetAllFunctionsForApp(appID string) ([]Function, error) { + response, err := p.makeRequest("GET", fmt.Sprintf(FunctionsApiEndpoint, appID), nil) + if err != nil { + return nil, errors.New("that app ID wasn't recognised as linked to your account") + } + functions := []Function{} + err = json.Unmarshal([]byte(response), &functions) + if err != nil { + return nil, errors.New("Response from Pusher API was not valid json, please retry") + } + return functions, nil +} + +func (p *PusherApi) CreateFunction(appID string, name string, events []string, body string) (Function, error) { + encoded := base64.StdEncoding.EncodeToString([]byte(body)) + + request := NewFunctionRequestBody(name, events, encoded) + + requestJson, err := json.Marshal(&request) + if err != nil { + return Function{}, errors.New("Could not create function") + } + response, err := p.makeRequest("POST", fmt.Sprintf(FunctionsApiEndpoint, appID), requestJson) + if err != nil { + switch err.(type) { + case *HttpStatusError: + e := err.(*HttpStatusError) + switch e.StatusCode { + case http.StatusUnprocessableEntity: + return Function{}, errors.New(response) + default: + return Function{}, errors.New("Pusher encountered an error, please retry") + } + default: + return Function{}, errors.New("Pusher encountered an error, please retry") + } + } + + function := Function{} + err = json.Unmarshal([]byte(response), &function) + if err != nil { + return Function{}, errors.New("Response from Pusher API was not valid json, please retry") + } + return function, nil +} + +func (p *PusherApi) DeleteFunction(appID string, functionID string) error { + _, err := p.makeRequest("DELETE", fmt.Sprintf(FunctionApiEndpoint, appID, functionID), nil) + if err != nil { + switch err.(type) { + case *HttpStatusError: + e := err.(*HttpStatusError) + switch e.StatusCode { + case http.StatusNotFound: + return fmt.Errorf("Funciton with id: %s, could not be found", functionID) + default: + return errors.New("Pusher encountered an error, please retry") + } + default: + return errors.New("Pusher encountered an error, please retry") + } + } + + return nil +} + +func (p *PusherApi) GetFunction(appID string, functionID string) (Function, error) { + response, err := p.makeRequest("GET", fmt.Sprintf(FunctionApiEndpoint, appID, functionID), nil) + if err != nil { + switch err.(type) { + case *HttpStatusError: + e := err.(*HttpStatusError) + if e.StatusCode == http.StatusNotFound { + return Function{}, errors.New("Function could not be found") + } else { + return Function{}, errors.New("Pusher encountered an error, please retry") + } + default: + return Function{}, errors.New("Pusher encountered an error, please retry") + } + } + function := Function{} + err = json.Unmarshal([]byte(response), &function) + if err != nil { + return Function{}, errors.New("Response from Pusher API was not valid json, please retry") + } + decodedBody, err := base64.StdEncoding.DecodeString(function.Body) + if err != nil { + return Function{}, errors.New("Response from Pusher API did not include a valid function Body, please retry") + } + function.Body = string(decodedBody) + return function, nil +} + +func (p *PusherApi) UpdateFunction(appID string, functionID string, name string, events []string, body string) (Function, error) { + encoded := base64.StdEncoding.EncodeToString([]byte(body)) + + request := NewFunctionRequestBody(name, events, encoded) + + requestJson, err := json.Marshal(&request) + if err != nil { + return Function{}, errors.New("Could not serialize function") + } + response, err := p.makeRequest("PUT", fmt.Sprintf(FunctionApiEndpoint, appID, functionID), requestJson) + if err != nil { + switch err.(type) { + case *HttpStatusError: + e := err.(*HttpStatusError) + if e.StatusCode == http.StatusUnprocessableEntity { + return Function{}, errors.New(response) + } else { + return Function{}, errors.New("Pusher encountered an error, please retry") + } + default: + return Function{}, errors.New("Pusher encountered an error, please retry") + } + } + + function := Function{} + err = json.Unmarshal([]byte(response), &function) + if err != nil { + return Function{}, errors.New("Response from Pusher API was not valid json, please retry") + } + return function, nil +} diff --git a/api/function_test.go b/api/function_test.go new file mode 100644 index 0000000..2e9759f --- /dev/null +++ b/api/function_test.go @@ -0,0 +1,273 @@ +package api + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/theherk/viper" +) + +func TestGetAllFunctionsForAppSuccess(t *testing.T) { + appID := "123" + api := NewMockPusherAPI(t). + Expect(fmt.Sprintf(FunctionsApiEndpoint, appID)). + Status(http.StatusOK). + Body(`[{"id":1,"name":"function1"}]`). + Start() + defer api.Stop() + + p := NewPusherApi() + actualFunctions, err := p.GetAllFunctionsForApp(appID) + if err != nil { + t.Fatal(err) + } + + actualFunctionsCount := len(actualFunctions) + expectedFunctionsCount := 1 + if actualFunctionsCount != expectedFunctionsCount { + t.Errorf("expected %d functions, got: %d functions", expectedFunctionsCount, actualFunctionsCount) + } + + expectedFunction := Function{ID: 1, Name: "function1"} + actualFunction := actualFunctions[0] + if !cmp.Equal(actualFunction, expectedFunction) { + t.Errorf("expected %v, got: %v", expectedFunction, actualFunction) + } +} + +func TestGetAllFunctionsError(t *testing.T) { + appID := "123" + api := NewMockPusherAPI(t). + Expect(fmt.Sprintf(FunctionsApiEndpoint, appID)). + Status(http.StatusInternalServerError). + Body(``). + Start() + defer api.Stop() + + p := NewPusherApi() + _, err := p.GetAllFunctionsForApp(appID) + if err == nil { + t.Error("unsuccesful api response should return an error") + } +} + +func TestCreateFunctionSuccess(t *testing.T) { + appID := "123" + api := NewMockPusherAPI(t). + Expect(fmt.Sprintf(FunctionsApiEndpoint, appID)). + Status(http.StatusOK). + Body(`{"id": 123,"name":"my-function"}`). + Start() + defer api.Stop() + + p := NewPusherApi() + actualFunction, err := p.CreateFunction(appID, "my-function", []string{"my-event"}, "some body") + if err != nil { + t.Fatal(err) + } + + expectedFunction := Function{ID: 123, Name: "my-function"} + if !cmp.Equal(actualFunction, expectedFunction) { + t.Errorf("expected %v, got: %v", expectedFunction, actualFunction) + } +} + +func TestCreateFunctionFailure(t *testing.T) { + appID := "123" + api := NewMockPusherAPI(t). + Expect(fmt.Sprintf(FunctionsApiEndpoint, appID)). + Status(http.StatusInternalServerError). + Body(``). + Start() + defer api.Stop() + + p := NewPusherApi() + _, err := p.CreateFunction(appID, "my-function", []string{"my-event"}, "some body") + if err == nil { + t.Error("unsuccesful api response should return an error") + } +} + +func TestDeleteSuccess(t *testing.T) { + appID := "123" + functionID := "456" + api := NewMockPusherAPI(t). + Expect(fmt.Sprintf(FunctionApiEndpoint, appID, functionID)). + Status(http.StatusOK). + Start() + defer api.Stop() + + p := NewPusherApi() + err := p.DeleteFunction(appID, functionID) + if err != nil { + t.Fatal(err) + } +} + +func TestDeleteFailure(t *testing.T) { + appID := "123" + functionID := "456" + api := NewMockPusherAPI(t). + Expect(fmt.Sprintf(FunctionApiEndpoint, appID, functionID)). + Status(http.StatusInternalServerError). + Start() + defer api.Stop() + + p := NewPusherApi() + err := p.DeleteFunction(appID, functionID) + if err == nil { + t.Error("unsuccesful api response should return an error") + } +} + +func TestGetFunctionSuccess(t *testing.T) { + appID := "123" + functionID := "456" + api := NewMockPusherAPI(t). + Expect(fmt.Sprintf(FunctionApiEndpoint, appID, functionID)). + Status(http.StatusOK). + Body(`{"id":456,"name":"function1","events":["my-event","a-event"],"body":"bXktY29kZQ=="}`). + Start() + defer api.Stop() + + p := NewPusherApi() + expectedFunction := Function{ID: 456, Name: "function1", Events: []string{"my-event", "a-event"}, Body: "my-code"} + actualFunction, err := p.GetFunction(appID, functionID) + if err != nil { + t.Fatal(err) + } + if !cmp.Equal(actualFunction, expectedFunction) { + t.Errorf("expected %v, got: %v", expectedFunction, actualFunction) + } +} + +func TestGetFunctionApiError(t *testing.T) { + appID := "123" + functionID := "456" + api := NewMockPusherAPI(t). + Expect(fmt.Sprintf(FunctionApiEndpoint, appID, functionID)). + Status(http.StatusInternalServerError). + Body(``). + Start() + defer api.Stop() + + p := NewPusherApi() + _, err := p.GetFunction(appID, functionID) + if err == nil { + t.Fatal("Expected GetFunction to return an error when the http call fails") + } +} + +func TestUpdateFunctionSuccess(t *testing.T) { + appID := "123" + functionID := "456" + api := NewMockPusherAPI(t). + Expect(fmt.Sprintf(FunctionApiEndpoint, appID, functionID)). + Status(http.StatusOK). + Body(`{"id": 123,"name":"my-function"}`). + Start() + defer api.Stop() + + p := NewPusherApi() + actualFunction, err := p.UpdateFunction(appID, functionID, "my-function", []string{"my-event"}, "some body") + if err != nil { + t.Fatal(err) + } + + expectedFunction := Function{ID: 123, Name: "my-function"} + if !cmp.Equal(actualFunction, expectedFunction) { + t.Errorf("expected %v, got: %v", expectedFunction, actualFunction) + } +} + +func TestUpdateFunctionFailure(t *testing.T) { + appID := "123" + functionID := "456" + api := NewMockPusherAPI(t). + Expect(fmt.Sprintf(FunctionApiEndpoint, appID, functionID)). + Status(http.StatusInternalServerError). + Body(``). + Start() + defer api.Stop() + + p := NewPusherApi() + _, err := p.UpdateFunction(appID, functionID, "my-function", []string{"my-event"}, "some body") + if err == nil { + t.Error("unsuccesful api response should return an error") + } +} + +type MockPusherAPI struct { + t *testing.T + endpoints map[string]*MockEndpoint + server *httptest.Server +} + +func NewMockPusherAPI(t *testing.T) *MockPusherAPI { + return &MockPusherAPI{ + t: t, + endpoints: make(map[string]*MockEndpoint), + } +} + +func (m *MockPusherAPI) Expect(path string) *MockEndpoint { + e := m.newMockEndpoint() + m.endpoints[path] = e + return e +} + +func (m *MockPusherAPI) Start() *MockPusherAPI { + m.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Accept") != "application/json" { + m.t.Errorf("Expected Accept: application/json header, got: %s", r.Header.Get("Accept")) + } + + endpoint := m.endpoints[r.URL.Path] + if endpoint != nil { + endpoint.WriteResponse(w) + } else { + m.t.Errorf("unxpected request path: %s", r.URL.Path) + } + })) + viper.Set("endpoint", m.server.URL) + viper.Set("token", "foo") + return m +} + +func (m *MockPusherAPI) Stop() { + m.server.Close() +} + +func (m *MockPusherAPI) newMockEndpoint() *MockEndpoint { + return &MockEndpoint{ + api: m, + } +} + +type MockEndpoint struct { + status int + body string + api *MockPusherAPI +} + +func (m *MockEndpoint) Status(code int) *MockEndpoint { + m.status = code + return m +} + +func (m *MockEndpoint) Body(body string) *MockEndpoint { + m.body = body + return m +} + +func (m *MockEndpoint) Start() *MockPusherAPI { + return m.api.Start() +} + +func (m *MockEndpoint) WriteResponse(w http.ResponseWriter) { + w.WriteHeader(m.status) + w.Write([]byte(m.body)) +} diff --git a/api/shared.go b/api/shared.go index 1883b3c..b101ab2 100644 --- a/api/shared.go +++ b/api/shared.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "fmt" "io/ioutil" "net/http" "time" @@ -11,17 +12,37 @@ import ( ) var ( - httpClient = &http.Client{Timeout: 5 * time.Second} + httpClient = &http.Client{Timeout: 10 * time.Second} ) -func makeRequest(reqtype string, path string, body []byte) (string, error) { - req, err := http.NewRequest(reqtype, viper.GetString("endpoint")+path, bytes.NewBuffer(body)) +type HttpStatusError struct { + StatusCode int +} + +func (e *HttpStatusError) Error() string { + return fmt.Sprintf("http status code: %d", e.StatusCode) +} + +type PusherApi struct { +} + +func NewPusherApi() *PusherApi { + return &PusherApi{} +} + +func (p *PusherApi) requestUrl(path string) string { + return viper.GetString("endpoint") + path +} + +func (p *PusherApi) makeRequest(reqtype string, path string, body []byte) (string, error) { + req, err := http.NewRequest(reqtype, p.requestUrl(path), bytes.NewBuffer(body)) if err != nil { return "", err } req.Header.Set("Content-type", "application/json") req.Header.Set("Authorization", "Token token="+viper.GetString("token")) req.Header.Set("User-Agent", "PusherCLI/"+config.GetVersion()) + req.Header.Set("Accept", "application/json") resp, err := httpClient.Do(req) if err != nil { return "", err @@ -34,5 +55,11 @@ func makeRequest(reqtype string, path string, body []byte) (string, error) { return "", err } + if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) { + return string(respBody), &HttpStatusError{ + StatusCode: resp.StatusCode, + } + } + return string(respBody), nil } diff --git a/api/token.go b/api/token.go index e8133c8..4f34f43 100644 --- a/api/token.go +++ b/api/token.go @@ -13,9 +13,9 @@ type AppToken struct { const getTokensAPIEndpoint = "/apps/%s/tokens.json" // Interpolate with `appId` -func GetAllTokensForApp(appId string) ([]AppToken, error) { - validateKeyOrDie() - response, err := makeRequest("GET", fmt.Sprintf(getTokensAPIEndpoint, appId), nil) +func (p *PusherApi) GetAllTokensForApp(appId string) ([]AppToken, error) { + p.validateKeyOrDie() + response, err := p.makeRequest("GET", fmt.Sprintf(getTokensAPIEndpoint, appId), nil) if err != nil { return nil, err } @@ -27,8 +27,8 @@ func GetAllTokensForApp(appId string) ([]AppToken, error) { return tokens, nil } -func GetToken(appId string) (*AppToken, error) { - tokens, err := GetAllTokensForApp(appId) +func (p *PusherApi) GetToken(appId string) (*AppToken, error) { + tokens, err := p.GetAllTokensForApp(appId) if err != nil { return nil, err } diff --git a/commands/auth/login.go b/commands/auth/login.go index d0caddd..740b3f4 100644 --- a/commands/auth/login.go +++ b/commands/auth/login.go @@ -39,8 +39,9 @@ var Login = &cobra.Command{ //APIKeyValid returns true if the stored API key is valid. func APIKeyValid() bool { + p := api.NewPusherApi() if viper.GetString("token") != "" { - _, err := api.GetAllApps() + _, err := p.GetAllApps() if err == nil { return true } diff --git a/commands/channels/apps.go b/commands/channels/apps.go index 5a4e5cc..5e13fc4 100644 --- a/commands/channels/apps.go +++ b/commands/channels/apps.go @@ -25,7 +25,8 @@ var Apps = &cobra.Command{ return } - apps, err := api.GetAllApps() + p := api.NewPusherApi() + apps, err := p.GetAllApps() if err != nil { fmt.Println("Failed to retrieve the list of apps.") os.Exit(1) diff --git a/commands/channels/auth_server.go b/commands/channels/auth_server.go index 3b94e87..c7e9e9b 100644 --- a/commands/channels/auth_server.go +++ b/commands/channels/auth_server.go @@ -34,14 +34,15 @@ var LocalAuthServer = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) os.Exit(1) diff --git a/commands/channels/channel_info.go b/commands/channels/channel_info.go index 3390379..fb2f0c2 100644 --- a/commands/channels/channel_info.go +++ b/commands/channels/channel_info.go @@ -29,14 +29,15 @@ var ChannelInfo = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) os.Exit(1) diff --git a/commands/channels/functions.go b/commands/channels/functions.go new file mode 100644 index 0000000..aaffeab --- /dev/null +++ b/commands/channels/functions.go @@ -0,0 +1,188 @@ +package channels + +import ( + "fmt" + "io" + "io/fs" + "strconv" + "strings" + + "encoding/json" + + "github.com/olekukonko/tablewriter" + "github.com/pusher/cli/api" + "github.com/pusher/cli/commands" + "github.com/spf13/cobra" +) + +func NewFunctionsCommand(pusher api.FunctionService, fs fs.ReadFileFS) *cobra.Command { + cmd := &cobra.Command{ + Use: "functions", + Short: "Manage functions for a Channels app", + Args: cobra.NoArgs, + } + cmd.PersistentFlags().StringVar(&commands.AppID, "app_id", "", "Channels App ID") + cmd.MarkPersistentFlagRequired("app_id") + cmd.AddCommand(NewFunctionsListCommand(pusher)) + cmd.AddCommand(NewFunctionsCreateCommand(pusher, fs)) + cmd.AddCommand(NewFunctionDeleteCommand(pusher)) + cmd.AddCommand(NewFunctionGetCommand(pusher)) + cmd.AddCommand(NewFunctionsUpdateCommand(pusher, fs)) + return cmd +} + +var Functions = &cobra.Command{ + Use: "functions", + Short: "Manage functions for a Channels app", + Args: cobra.NoArgs, +} + +func newTable(out io.Writer) *tablewriter.Table { + table := tablewriter.NewWriter(out) + table.SetBorder(false) + table.SetRowLine(false) + table.SetHeaderLine(false) + table.SetColumnSeparator("") + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + return table +} + +func NewFunctionsListCommand(functionService api.FunctionService) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List functions for an Channels app", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + functions, err := functionService.GetAllFunctionsForApp(commands.AppID) + if err != nil { + return err + } + + if commands.OutputAsJSON { + functionsJSONBytes, _ := json.Marshal(functions) + cmd.Println(string(functionsJSONBytes)) + } else { + table := newTable(cmd.OutOrStdout()) + table.SetHeader([]string{"ID", "Name", "Events"}) + for _, function := range functions { + table.Append([]string{strconv.Itoa(function.ID), function.Name, strings.Join(function.Events, ",")}) + } + table.Render() + } + return nil + }, + } + cmd.PersistentFlags().BoolVar(&commands.OutputAsJSON, "json", false, "") + return cmd +} + +func NewFunctionsCreateCommand(functionService api.FunctionService, fs fs.ReadFileFS) *cobra.Command { + cmd := &cobra.Command{ + Use: "create ", + Short: "Create a function for a Channels app", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + code, err := fs.ReadFile(args[0]) + if err != nil { + return fmt.Errorf("could not create function: %s does not exist", args[0]) + } + + function, err := functionService.CreateFunction(commands.AppID, commands.FunctionName, commands.FunctionEvents, string(code)) + if err != nil { + return err + } + + if commands.OutputAsJSON { + functionJSONBytes, _ := json.Marshal(function) + fmt.Fprintln(cmd.OutOrStdout(), string(functionJSONBytes)) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "created function %s with id: %d\n", function.Name, function.ID) + } + return nil + }, + } + cmd.PersistentFlags().BoolVar(&commands.OutputAsJSON, "json", false, "") + cmd.PersistentFlags().StringVar(&commands.FunctionName, "name", "", "Function name") + cmd.MarkPersistentFlagRequired("name") + cmd.PersistentFlags().StringSliceVar(&commands.FunctionEvents, "events", []string{}, "Channel events that trigger this function") + return cmd +} + +func NewFunctionDeleteCommand(functionService api.FunctionService) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a function from a Channels app", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + err := functionService.DeleteFunction(commands.AppID, args[0]) + if err != nil { + return err + } + + fmt.Fprintf(cmd.OutOrStdout(), "deleted function %s\n", args[0]) + return nil + }, + } + return cmd +} + +func NewFunctionGetCommand(functionService api.FunctionService) *cobra.Command { + cmd := &cobra.Command{ + Use: "get ", + Short: "Get a function for a Channels app", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + fn, err := functionService.GetFunction(commands.AppID, args[0]) + if err != nil { + return err + } + + if commands.OutputAsJSON { + functionsJSONBytes, _ := json.Marshal(fn) + cmd.Println(string(functionsJSONBytes)) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "ID: %v\n", fn.ID) + fmt.Fprintf(cmd.OutOrStdout(), "Name: %v\n", fn.Name) + fmt.Fprintf(cmd.OutOrStdout(), "Events: %v\n", strings.Join(fn.Events, ",")) + fmt.Fprintf(cmd.OutOrStdout(), "Body: %v\n", fn.Body) + } + return nil + }, + } + cmd.PersistentFlags().BoolVar(&commands.OutputAsJSON, "json", false, "") + return cmd +} + +func NewFunctionsUpdateCommand(functionService api.FunctionService, fs fs.ReadFileFS) *cobra.Command { + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a function for a Channels app", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + filePath := args[1] + code, err := fs.ReadFile(filePath) + if err != nil { + return fmt.Errorf("could not update function: %s does not exist", filePath) + } + + function, err := functionService.UpdateFunction(commands.AppID, args[0], commands.FunctionName, commands.FunctionEvents, string(code)) + if err != nil { + return err + } + + if commands.OutputAsJSON { + functionJSONBytes, _ := json.Marshal(function) + fmt.Fprintln(cmd.OutOrStdout(), string(functionJSONBytes)) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "updated function: %d\n", function.ID) + } + return nil + }, + } + cmd.PersistentFlags().BoolVar(&commands.OutputAsJSON, "json", false, "") + cmd.PersistentFlags().StringVar(&commands.FunctionName, "name", "", "Function name") + cmd.MarkPersistentFlagRequired("name") + cmd.PersistentFlags().StringSliceVar(&commands.FunctionEvents, "events", []string{}, "Channel events that trigger this function") + return cmd +} diff --git a/commands/channels/functions_test.go b/commands/channels/functions_test.go new file mode 100644 index 0000000..fbe3391 --- /dev/null +++ b/commands/channels/functions_test.go @@ -0,0 +1,351 @@ +package channels + +import ( + "bufio" + "bytes" + "errors" + "io" + "io/fs" + "strings" + "testing" + "testing/fstest" + + "github.com/golang/mock/gomock" + "github.com/pusher/cli/api" + "github.com/pusher/cli/mocks" + "github.com/theherk/viper" +) + +func TestFunctionsListSuccess(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + mockFunctionService. + EXPECT(). + GetAllFunctionsForApp("1"). + Return([]api.Function{{ID: 1, Name: "function1", Events: []string{"a-event"}}, {ID: 2, Name: "function2", Events: []string{"b-event"}}}, nil). + Times(1) + + b, _ := executeCommand([]string{"list", "--app_id", "1"}, mockFunctionService, fstest.MapFS{}) + scanner := bufio.NewScanner(b) + + expectedRows := []string{ + "ID NAME EVENTS", + "1 function1 a-event", + "2 function2 b-event", + } + + for i, expectedRow := range expectedRows { + scanner.Scan() + actualRow := strings.Trim(scanner.Text(), " ") + if actualRow != expectedRow { + t.Errorf("expected row %d to equal \"%s\" got \"%s\"", i, expectedRow, actualRow) + } + } +} + +func TestFunctionsListError(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + mockFunctionService. + EXPECT(). + GetAllFunctionsForApp("1"). + Return([]api.Function{}, errors.New("Api error")). + Times(1) + + _, err := executeCommand([]string{"list", "--app_id", "1"}, mockFunctionService, fstest.MapFS{}) + if err == nil { + t.Errorf("expected error, non received") + } +} + +func TestCreateSuccess(t *testing.T) { + fs := fstest.MapFS{ + "code.js": {Data: []byte("my-code")}, + } + mockCtrl := gomock.NewController(t) + mockCtrl.Finish() + + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + mockFunctionService. + EXPECT(). + CreateFunction("1", "my-function", []string{"my-event"}, "my-code"). + Return(api.Function{ID: 123, Name: "my-function"}, nil). + Times(1) + + b, _ := executeCommand([]string{"create", "code.js", "--app_id", "1", "--name", "my-function", "--events", "my-event"}, mockFunctionService, fs) + expectedOutput := "created function my-function with id: 123" + actualOutput, err := io.ReadAll(b) + if err != nil { + t.Error(err) + } + if string(actualOutput) != expectedOutput { + t.Errorf("expected output: %s, got: %s", expectedOutput, string(actualOutput)) + } +} + +func TestCreateFailure(t *testing.T) { + fs := fstest.MapFS{ + "code.js": {Data: []byte("my-code")}, + } + mockCtrl := gomock.NewController(t) + mockCtrl.Finish() + + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + mockFunctionService. + EXPECT(). + CreateFunction("1", "my-function", []string{"my-event"}, "my-code"). + Return(api.Function{}, errors.New("Api error")). + Times(1) + + _, err := executeCommand([]string{"create", "code.js", "--app_id", "1", "--name", "my-function", "--events", "my-event"}, mockFunctionService, fs) + if err == nil { + t.Error("expected Api error to be returned") + } +} + +func TestCreateFileDoesNotExist(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockCtrl.Finish() + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + + _, err := executeCommand([]string{"create", "code.js", "--app_id", "1", "--name", "my-function", "--events", "my-event"}, mockFunctionService, fstest.MapFS{}) + if err == nil { + t.Error("expected file not existing to return an error") + } +} + +func TestCreateRequiredParams(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockCtrl.Finish() + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + + params := [][]string{ + {"--app_id", "1"}, + {"--name", "my-function"}, + {"--events", "my-event"}, + } + + baseArgs := []string{"create", "code.js"} + + for i, requiredParam := range params { + args := make([]string, len(baseArgs)) + for j, param := range params { + if i != j { + args = append(args, param[0]) + args = append(args, param[1]) + } + } + _, err := executeCommand(args, mockFunctionService, fstest.MapFS{}) + if err == nil { + t.Errorf("expected %s to be required argument for create function command", requiredParam[0]) + } + } +} + +func TestDeleteSuccess(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockCtrl.Finish() + + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + mockFunctionService. + EXPECT(). + DeleteFunction("1", "123"). + Return(nil). + Times(1) + + b, _ := executeCommand([]string{"delete", "123", "--app_id", "1"}, mockFunctionService, fstest.MapFS{}) + expectedOutput := "deleted function 123" + actualOutput, err := io.ReadAll(b) + if err != nil { + t.Error(err) + } + if string(actualOutput) != expectedOutput { + t.Errorf("expected output: %s, got: %s", expectedOutput, string(actualOutput)) + } +} + +func TestDeleteRequiredArgs(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockCtrl.Finish() + + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + + _, err := executeCommand([]string{"delete", "--app_id", "1"}, mockFunctionService, fstest.MapFS{}) + if err == nil { + t.Error("expected args[0] to be required") + } + + _, err = executeCommand([]string{"delete", "123"}, mockFunctionService, fstest.MapFS{}) + if err == nil { + t.Error("expected --app_id to be required") + } +} + +func TestDeleteFailure(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockCtrl.Finish() + + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + mockFunctionService. + EXPECT(). + DeleteFunction("1", "123"). + Return(errors.New("Api error")). + Times(1) + + _, err := executeCommand([]string{"delete", "123", "--app_id", "1"}, mockFunctionService, fstest.MapFS{}) + if err == nil { + t.Error("expected Api error to be returned") + } +} + +func TestGetSuccess(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockCtrl.Finish() + + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + mockFunctionService. + EXPECT(). + GetFunction("1", "123"). + Return(api.Function{ID: 123, Name: "my-function", Events: []string{"my-event"}, Body: "my-code"}, nil). + Times(1) + + b, _ := executeCommand([]string{"get", "123", "--app_id", "1"}, mockFunctionService, fstest.MapFS{}) + expectedOutput := "ID: 123\nName: my-function\nEvents: my-event\nBody: my-code\n" + actualOutput, err := io.ReadAll(b) + if err != nil { + t.Error(err) + } + if string(actualOutput) != expectedOutput { + t.Errorf("expected output: %s, got: %s", expectedOutput, string(actualOutput)) + } +} + +func TestGetRequiredArgs(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockCtrl.Finish() + + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + + _, err := executeCommand([]string{"get", "--app_id", "1"}, mockFunctionService, fstest.MapFS{}) + if err == nil { + t.Error("expected args[0] to be required") + } + + _, err = executeCommand([]string{"get", "123"}, mockFunctionService, fstest.MapFS{}) + if err == nil { + t.Error("expected --app_id to be required") + } +} + +func TestGetFailure(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockCtrl.Finish() + + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + mockFunctionService. + EXPECT(). + GetFunction("1", "123"). + Return(api.Function{}, errors.New("Api error")). + Times(1) + + _, err := executeCommand([]string{"get", "123", "--app_id", "1"}, mockFunctionService, fstest.MapFS{}) + if err == nil { + t.Error("expected Api error to be returned") + } +} + +func TestUpdateSuccess(t *testing.T) { + fs := fstest.MapFS{ + "code.js": {Data: []byte("my-code")}, + } + mockCtrl := gomock.NewController(t) + mockCtrl.Finish() + + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + mockFunctionService. + EXPECT(). + UpdateFunction("1", "456", "my-function", []string{"my-event"}, "my-code"). + Return(api.Function{ID: 123, Name: "my-function"}, nil). + Times(1) + + b, _ := executeCommand([]string{"update", "456", "code.js", "--app_id", "1", "--name", "my-function", "--events", "my-event"}, mockFunctionService, fs) + expectedOutput := "updated function: 123" + actualOutput, err := io.ReadAll(b) + if err != nil { + t.Error(err) + } + if string(actualOutput) != expectedOutput { + t.Errorf("expected output: %s, got: %s", expectedOutput, string(actualOutput)) + } +} + +func TestUpdateFailure(t *testing.T) { + fs := fstest.MapFS{ + "code.js": {Data: []byte("my-code")}, + } + mockCtrl := gomock.NewController(t) + mockCtrl.Finish() + + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + mockFunctionService. + EXPECT(). + UpdateFunction("1", "456", "my-function", []string{"my-event"}, "my-code"). + Return(api.Function{}, errors.New("Api error")). + Times(1) + + _, err := executeCommand([]string{"update", "456", "code.js", "--app_id", "1", "--name", "my-function", "--events", "my-event"}, mockFunctionService, fs) + if err == nil { + t.Error("expected Api error to be returned") + } +} + +func TestUpdateFileDoesNotExist(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockCtrl.Finish() + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + + _, err := executeCommand([]string{"update", "456", "code.js", "--app_id", "1", "--name", "my-function", "--events", "my-event"}, mockFunctionService, fstest.MapFS{}) + if err == nil { + t.Error("expected file not existing to return an error") + } +} + +func TestUpdateRequiredParams(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockCtrl.Finish() + mockFunctionService := mocks.NewMockFunctionService(mockCtrl) + + params := [][]string{ + {"--app_id", "1"}, + {"--name", "my-function"}, + {"--events", "my-event"}, + } + + baseArgs := []string{"update", "456", "code.js"} + + for i, requiredParam := range params { + args := make([]string, len(baseArgs)) + for j, param := range params { + if i != j { + args = append(args, param[0]) + args = append(args, param[1]) + } + } + _, err := executeCommand(args, mockFunctionService, fstest.MapFS{}) + if err == nil { + t.Errorf("expected %s to be required argument for update function command", requiredParam[0]) + } + } +} +func executeCommand(args []string, a api.FunctionService, fs fs.ReadFileFS) (*bytes.Buffer, error) { + cmd := NewFunctionsCommand(a, fs) + b := bytes.NewBufferString("") + viper.Set("token", "foo") + cmd.SetOut(b) + cmd.SetArgs(args) + return b, cmd.Execute() +} diff --git a/commands/channels/generate_client.go b/commands/channels/generate_client.go index 1a3c08c..727952e 100644 --- a/commands/channels/generate_client.go +++ b/commands/channels/generate_client.go @@ -29,14 +29,15 @@ var GenerateWeb = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) return diff --git a/commands/channels/generate_server.go b/commands/channels/generate_server.go index 8142a9a..a010c52 100644 --- a/commands/channels/generate_server.go +++ b/commands/channels/generate_server.go @@ -29,14 +29,15 @@ var GeneratePhp = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) return @@ -78,14 +79,15 @@ var GeneratePython = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) return diff --git a/commands/channels/list_channels.go b/commands/channels/list_channels.go index e443797..7bd1873 100644 --- a/commands/channels/list_channels.go +++ b/commands/channels/list_channels.go @@ -23,14 +23,15 @@ var ListChannels = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) os.Exit(1) diff --git a/commands/channels/subscribe.go b/commands/channels/subscribe.go index eedc77e..4c84032 100644 --- a/commands/channels/subscribe.go +++ b/commands/channels/subscribe.go @@ -30,14 +30,15 @@ var Subscribe = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) os.Exit(1) diff --git a/commands/channels/tokens.go b/commands/channels/tokens.go index ab8d17c..8efcf75 100644 --- a/commands/channels/tokens.go +++ b/commands/channels/tokens.go @@ -25,7 +25,8 @@ var Tokens = &cobra.Command{ return } - tokens, err := api.GetAllTokensForApp(commands.AppID) + p := api.NewPusherApi() + tokens, err := p.GetAllTokensForApp(commands.AppID) if err != nil { fmt.Printf("Failed to retrieve the list of tokens: %s\n", err.Error()) os.Exit(1) diff --git a/commands/channels/trigger.go b/commands/channels/trigger.go index e547de4..b8ff78e 100644 --- a/commands/channels/trigger.go +++ b/commands/channels/trigger.go @@ -41,14 +41,15 @@ var Trigger = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) os.Exit(1) diff --git a/commands/shared_flags.go b/commands/shared_flags.go index 5846033..719f227 100644 --- a/commands/shared_flags.go +++ b/commands/shared_flags.go @@ -8,3 +8,5 @@ var OutputAsJSON bool var FilterByPrefix string var FetchUserCount bool var FetchSubscriptionCount bool +var FunctionName string +var FunctionEvents []string diff --git a/go.mod b/go.mod index bd57d94..bb35ba1 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,8 @@ go 1.15 require ( github.com/fatih/color v1.13.0 github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/google/go-cmp v0.5.8 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/google/go-cmp v0.5.9 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/magiconair/properties v1.8.6 // indirect diff --git a/go.sum b/go.sum index 9b03353..62426cb 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,8 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -105,6 +107,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -205,6 +209,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -253,6 +258,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -284,6 +290,7 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -304,6 +311,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -337,8 +345,10 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -405,6 +415,7 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index f0f7e4e..920c2ca 100644 --- a/main.go +++ b/main.go @@ -2,8 +2,10 @@ package main import ( "fmt" + "io/fs" "os" + "github.com/pusher/cli/api" "github.com/pusher/cli/commands/auth" "github.com/pusher/cli/commands/channels" "github.com/pusher/cli/commands/cli" @@ -11,13 +13,18 @@ import ( "github.com/spf13/cobra" ) +type osFS struct{} + +func (osFS) Open(name string) (fs.File, error) { return os.Open(name) } +func (osFS) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) } + func main() { config.Init() var rootCmd = &cobra.Command{Use: "pusher", Short: "A CLI for your Pusher account. Find out more at https://pusher.com"} var Apps = &cobra.Command{Use: "apps", Short: "Manage your Channels Apps"} - Apps.AddCommand(channels.Apps, channels.Tokens, channels.Subscribe, channels.Trigger, channels.ListChannels, channels.ChannelInfo) + Apps.AddCommand(channels.Apps, channels.Tokens, channels.Subscribe, channels.Trigger, channels.ListChannels, channels.ChannelInfo, channels.NewFunctionsCommand(api.NewPusherApi(), osFS{})) var Generate = &cobra.Command{Use: "generate", Short: "Generate a Channels client, server, or Authorisation server"} diff --git a/mocks/mock_functions.go b/mocks/mock_functions.go new file mode 100644 index 0000000..f38fd93 --- /dev/null +++ b/mocks/mock_functions.go @@ -0,0 +1,109 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: api/function.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + api "github.com/pusher/cli/api" +) + +// MockFunctionService is a mock of FunctionService interface. +type MockFunctionService struct { + ctrl *gomock.Controller + recorder *MockFunctionServiceMockRecorder +} + +// MockFunctionServiceMockRecorder is the mock recorder for MockFunctionService. +type MockFunctionServiceMockRecorder struct { + mock *MockFunctionService +} + +// NewMockFunctionService creates a new mock instance. +func NewMockFunctionService(ctrl *gomock.Controller) *MockFunctionService { + mock := &MockFunctionService{ctrl: ctrl} + mock.recorder = &MockFunctionServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFunctionService) EXPECT() *MockFunctionServiceMockRecorder { + return m.recorder +} + +// CreateFunction mocks base method. +func (m *MockFunctionService) CreateFunction(appID, name string, events []string, body string) (api.Function, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateFunction", appID, name, events, body) + ret0, _ := ret[0].(api.Function) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateFunction indicates an expected call of CreateFunction. +func (mr *MockFunctionServiceMockRecorder) CreateFunction(appID, name, events, body interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateFunction", reflect.TypeOf((*MockFunctionService)(nil).CreateFunction), appID, name, events, body) +} + +// DeleteFunction mocks base method. +func (m *MockFunctionService) DeleteFunction(appID, functionID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteFunction", appID, functionID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteFunction indicates an expected call of DeleteFunction. +func (mr *MockFunctionServiceMockRecorder) DeleteFunction(appID, functionID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFunction", reflect.TypeOf((*MockFunctionService)(nil).DeleteFunction), appID, functionID) +} + +// GetAllFunctionsForApp mocks base method. +func (m *MockFunctionService) GetAllFunctionsForApp(name string) ([]api.Function, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllFunctionsForApp", name) + ret0, _ := ret[0].([]api.Function) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllFunctionsForApp indicates an expected call of GetAllFunctionsForApp. +func (mr *MockFunctionServiceMockRecorder) GetAllFunctionsForApp(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllFunctionsForApp", reflect.TypeOf((*MockFunctionService)(nil).GetAllFunctionsForApp), name) +} + +// GetFunction mocks base method. +func (m *MockFunctionService) GetFunction(appID, functionID string) (api.Function, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFunction", appID, functionID) + ret0, _ := ret[0].(api.Function) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFunction indicates an expected call of GetFunction. +func (mr *MockFunctionServiceMockRecorder) GetFunction(appID, functionID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFunction", reflect.TypeOf((*MockFunctionService)(nil).GetFunction), appID, functionID) +} + +// UpdateFunction mocks base method. +func (m *MockFunctionService) UpdateFunction(appID, functionID, name string, events []string, body string) (api.Function, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateFunction", appID, functionID, name, events, body) + ret0, _ := ret[0].(api.Function) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateFunction indicates an expected call of UpdateFunction. +func (mr *MockFunctionServiceMockRecorder) UpdateFunction(appID, functionID, name, events, body interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateFunction", reflect.TypeOf((*MockFunctionService)(nil).UpdateFunction), appID, functionID, name, events, body) +}